Inject Bean completion proposal via Rewrite recipes

This commit is contained in:
vudayani
2024-10-03 11:27:30 +05:30
committed by aboyko
parent ca1097058f
commit 41fa3f4a12
20 changed files with 2382 additions and 19 deletions

View File

@@ -14,6 +14,7 @@ package org.springframework.ide.vscode.commons.languageserver.completion;
import java.util.Optional;
import java.util.function.Supplier;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.util.Renderable;
@@ -57,4 +58,9 @@ public interface ICompletionProposal {
}
};
}
default Optional<Command> getCommand() {
return Optional.empty();
}
}

View File

@@ -300,6 +300,7 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
}
List<Object> commands = new ArrayList<>(2);
completion.getCommand().ifPresent(commands::add);
if (LspClient.currentClient() != LspClient.Client.ECLIPSE) {
/*
* Eclipse client always send completionItem resolve request before applying completion.
@@ -352,7 +353,9 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
}
}
if (subCommands.size() == 1) {
item.setCommand((Command)subCommands.get(0));
Object o = subCommands.get(0);
Command subCommand = o instanceof Command ? (Command) o : GSON.fromJson(o instanceof JsonElement ? (JsonElement) o : GSON.toJsonTree(o), Command.class);
item.setCommand(subCommand);
} else if (subCommands.isEmpty()) {
item.setCommand(null);
}

View File

@@ -315,7 +315,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, SpringInd
if (CODE_ACTION_COMMAND_ID.equals(params.getCommand())) {
Assert.isLegal(params.getArguments().size()==2);
QuickfixResolveParams quickfixParams = new QuickfixResolveParams(
((JsonPrimitive)params.getArguments().get(0)).getAsString(), params.getArguments().get(1)
params.getArguments().get(0) instanceof JsonPrimitive ? ((JsonPrimitive)params.getArguments().get(0)).getAsString() : params.getArguments().get(0).toString() , params.getArguments().get(1)
);
return quickfixResolve(quickfixParams)
.flatMap((QuickfixEdit edit) -> {

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2021 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring;
import java.util.Objects;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.properties.tree.Properties;
import org.openrewrite.yaml.tree.Yaml;
public class CommentOutSpringPropertyKey extends Recipe {
@Override
public String getDisplayName() {
return "Comment out Spring properties";
}
@Override
public String getDescription() {
return "Add comment to specified Spring properties, and comment out the property.";
}
@Option(displayName = "Property key",
description = "The name of the property key to comment out.",
example = "management.metrics.binders.files.enabled")
String propertyKey;
@Option(displayName = "Comment",
description = "Comment to replace the property key.",
example = "This property is deprecated and no longer applicable starting from Spring Boot 3.0.x")
String comment;
public CommentOutSpringPropertyKey(String propertyKey, String comment) {
super();
this.propertyKey = propertyKey;
this.comment = comment;
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
Recipe changeProperties = new org.openrewrite.properties.AddPropertyComment(propertyKey, comment, true);
Recipe changeYaml = new org.openrewrite.yaml.CommentOutProperty(propertyKey, comment, true);
return new TreeVisitor<Tree, ExecutionContext>() {
@Override
public @Nullable Tree preVisit(@NonNull Tree tree, ExecutionContext ctx) {
stopAfterPreVisit();
if (tree instanceof Properties.File) {
return changeProperties.getVisitor().visit(tree, ctx);
} else if (tree instanceof Yaml.Documents) {
return changeYaml.getVisitor().visit(tree, ctx);
}
return tree;
}
};
}
public String getPropertyKey() {
return propertyKey;
}
public void setPropertyKey(String propertyKey) {
this.propertyKey = propertyKey;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(comment, propertyKey);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
CommentOutSpringPropertyKey other = (CommentOutSpringPropertyKey) obj;
return Objects.equals(comment, other.comment) && Objects.equals(propertyKey, other.propertyKey);
}
}

View File

@@ -21,6 +21,7 @@ import org.eclipse.lsp4j.CreateFile;
import org.eclipse.lsp4j.DeleteFile;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ResourceOperation;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
@@ -71,6 +72,44 @@ public class ORDocUtils {
return Optional.empty();
}
public static Optional<DocumentEdits> computeDocumentEdits(WorkspaceEdit we, IDocument doc) {
if (!we.getDocumentChanges().isEmpty()) {
DocumentEdits edits = new DocumentEdits(doc, false);
List<Either<TextDocumentEdit, ResourceOperation>> changes = we.getDocumentChanges();
for (Either<TextDocumentEdit, ResourceOperation> change : changes) {
if (change.isLeft()) {
TextDocumentEdit textDocumentEdit = change.getLeft();
List<TextEdit> textEdits = textDocumentEdit.getEdits();
for (TextEdit textEdit : textEdits) {
Range range = textEdit.getRange();
Position start = range.getStart();
Position end = range.getEnd();
String newText = textEdit.getNewText();
try {
int startOffset = doc.getLineOffset(start.getLine()) + start.getCharacter();
int endOffset = doc.getLineOffset(end.getLine()) + end.getCharacter();
if (startOffset == endOffset) {
edits.insert(startOffset, newText);
} else if (newText.isEmpty()) {
edits.delete(startOffset, endOffset);
} else {
edits.replace(startOffset, endOffset, newText);
}
} catch (BadLocationException ex) {
log.error("Failed to apply text edit", ex);
}
}
}
}
return Optional.of(edits);
}
return Optional.empty();
}
public static Optional<TextDocumentEdit> computeTextDocEdit(TextDocument doc, String oldContent, String newContent, String changeAnnotationId) {
TextDocument newDoc = new TextDocument(null, LanguageId.PLAINTEXT, 0, newContent);
@@ -172,7 +211,7 @@ public class ORDocUtils {
addToWorkspaceEdit(documents, docUri, oldContent, newContent, changeAnnotationId, we);
}
}
public static void addToWorkspaceEdit(SimpleTextDocumentService documents, String docUri, String oldContent, String newContent, String changeAnnotationId, WorkspaceEdit we) {
if(oldContent == null) {
createNewFileEdit(docUri, newContent, changeAnnotationId, we);

View File

@@ -0,0 +1,135 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.java;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.NonNull;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.Tree;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JRightPadded;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.Space;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.java.tree.TypeTree;
import org.openrewrite.java.tree.TypeUtils;
import org.openrewrite.marker.Markers;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Udayani V
*/
public class AddFieldRecipe extends Recipe {
@Override
public String getDisplayName() {
return "Add field";
}
@Override
public String getDescription() {
return "Add field desccription.";
}
String fullyQualifiedName;
@NonNull
String classFqName;
@JsonCreator
public AddFieldRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedName, @NonNull @JsonProperty("classFqName") String classFqName) {
this.fullyQualifiedName = fullyQualifiedName;
this.classFqName = classFqName;
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
JavaType.FullyQualified fullyQualifiedType = JavaType.ShallowClass.build(fullyQualifiedName);
String fieldType = getFieldType(fullyQualifiedType);
String fieldName = getFieldName(fullyQualifiedType);
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
if (TypeUtils.isOfClassType(classDecl.getType(), classFqName)) {
// Check if the class already has the field
boolean hasOwnerRepoField = classDecl.getBody().getStatements().stream()
.filter(J.VariableDeclarations.class::isInstance).map(J.VariableDeclarations.class::cast)
.anyMatch(varDecl -> varDecl.getTypeExpression() != null
&& varDecl.getTypeExpression().toString().equals(fieldType));
if (!hasOwnerRepoField) {
J.VariableDeclarations newFieldDecl = new J.VariableDeclarations(
Tree.randomId(),
Space.build("\n\n", Collections.emptyList()),
Markers.EMPTY,
Collections.emptyList(),
List.of(
new J.Modifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, "private", J.Modifier.Type.Private, Collections.emptyList()),
new J.Modifier(Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, "final", J.Modifier.Type.Final, Collections.emptyList())
),
TypeTree.build(fieldType),
null,
Collections.emptyList(),
List.of(JRightPadded.build(new J.VariableDeclarations.NamedVariable(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, Collections.emptyList(), fieldName, fullyQualifiedType, null),
Collections.emptyList(),
null,
null
)))
);
Statement formattedNewFieldDecl = autoFormat(classDecl.getBody().withStatements(List.of(newFieldDecl)), ctx, getCursor()).getStatements().get(0);
List<Statement> newStatements = new ArrayList<>(classDecl.getBody().getStatements().size() + 1);
newStatements.add(formattedNewFieldDecl);
newStatements.addAll(classDecl.getBody().getStatements());
classDecl = classDecl.withBody(classDecl.getBody().withStatements(newStatements));
maybeAddImport(fullyQualifiedType.getFullyQualifiedName(), false);
}
return classDecl;
}
classDecl = (J.ClassDeclaration) super.visitClassDeclaration(classDecl, ctx);
return classDecl;
}
};
}
private static String getFieldName(JavaType.FullyQualified fullyQualifiedType) {
return Character.toLowerCase(fullyQualifiedType.getClassName().charAt(0)) + fullyQualifiedType.getClassName().substring(1);
}
private static String getFieldType(JavaType.FullyQualified fullyQualifiedType) {
if(fullyQualifiedType.getOwningClass() != null) {
String[] parts = fullyQualifiedType.getFullyQualifiedName().split("\\.");
if (parts.length < 2) {
return fullyQualifiedType.getClassName();
}
return parts[parts.length - 2] + "." + parts[parts.length - 1];
}
return fullyQualifiedType.getClassName();
}
}

View File

@@ -0,0 +1,316 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.java;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.jspecify.annotations.NonNull;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.NlsRewrite.Description;
import org.openrewrite.NlsRewrite.DisplayName;
import org.openrewrite.Recipe;
import org.openrewrite.Tree;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.JavaVisitor;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.J.Assignment;
import org.openrewrite.java.tree.J.Block;
import org.openrewrite.java.tree.J.ClassDeclaration;
import org.openrewrite.java.tree.J.MethodDeclaration;
import org.openrewrite.java.tree.J.VariableDeclarations;
import org.openrewrite.java.tree.JLeftPadded;
import org.openrewrite.java.tree.JRightPadded;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.JavaType.FullyQualified;
import org.openrewrite.java.tree.JavaType.ShallowClass;
import org.openrewrite.java.tree.Space;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.java.tree.TypeTree;
import org.openrewrite.java.tree.TypeUtils;
import org.openrewrite.marker.Markers;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Udayani V
*/
public class ConstructorInjectionRecipe extends Recipe {
@Override
public @DisplayName String getDisplayName() {
return "Add bean injection";
}
@Override
public @Description String getDescription() {
return "Add bean injection.";
}
@NonNull
String fullyQualifiedName;
@NonNull
String fieldName;
@NonNull
String classFqName;
@JsonCreator
public ConstructorInjectionRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedName,
@NonNull @JsonProperty("fieldName") String fieldName,
@NonNull @JsonProperty("classFqName") String classFqName) {
this.fullyQualifiedName = fullyQualifiedName;
this.fieldName = fieldName;
this.classFqName = classFqName;
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new CustomFieldIntoConstructorParameterVisitor(classFqName, fieldName);
}
class CustomFieldIntoConstructorParameterVisitor extends JavaVisitor<ExecutionContext> {
private final String classFqName;
private final String fieldName;
private static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
public CustomFieldIntoConstructorParameterVisitor(String classFqName, String fieldName) {
this.classFqName = classFqName;
this.fieldName = fieldName;
}
@Override
public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
if (TypeUtils.isOfClassType(classDecl.getType(), classFqName)) {
List<MethodDeclaration> constructors = classDecl.getBody().getStatements().stream()
.filter(J.MethodDeclaration.class::isInstance).map(J.MethodDeclaration.class::cast)
.filter(MethodDeclaration::isConstructor).collect(Collectors.toList());
boolean applicable = false;
if (constructors.isEmpty()) {
applicable = true;
} else if (constructors.size() == 1) {
MethodDeclaration c = constructors.get(0);
getCursor().putMessage("applicableConstructor", c);
applicable = isNotConstructorInitializingField(c, fieldName);
} else {
List<MethodDeclaration> autowiredConstructors = constructors.stream()
.filter(constr -> constr.getLeadingAnnotations().stream()
.map(a -> TypeUtils.asFullyQualified(a.getType())).filter(Objects::nonNull)
.map(FullyQualified::getFullyQualifiedName).anyMatch(AUTOWIRED::equals))
.limit(2).collect(Collectors.toList());
if (autowiredConstructors.size() == 1) {
MethodDeclaration c = autowiredConstructors.get(0);
getCursor().putMessage("applicableConstructor", autowiredConstructors.get(0));
applicable = isNotConstructorInitializingField(c, fieldName);
}
}
if (applicable) {
return super.visitClassDeclaration(classDecl, ctx);
}
}
return super.visitClassDeclaration(classDecl, ctx);
}
public static boolean isNotConstructorInitializingField(MethodDeclaration c, String fieldName) {
return c.getBody() == null || c.getBody().getStatements().stream().filter(J.Assignment.class::isInstance)
.map(J.Assignment.class::cast).noneMatch(a -> {
Expression expr = a.getVariable();
if (expr instanceof J.FieldAccess) {
J.FieldAccess fa = (J.FieldAccess) expr;
if (fieldName.equals(fa.getSimpleName()) && fa.getTarget() instanceof J.Identifier) {
J.Identifier target = (J.Identifier) fa.getTarget();
if ("this".equals(target.getSimpleName())) {
return true;
}
}
}
if (expr instanceof J.Identifier) {
JavaType.Variable fieldType = c.getMethodType().getDeclaringType().getMembers().stream()
.filter(v -> fieldName.equals(v.getName())).findFirst().orElse(null);
if (fieldType != null) {
J.Identifier identifier = (J.Identifier) expr;
return fieldType.equals(identifier.getFieldType());
}
}
return false;
});
}
@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable,
ExecutionContext ctx) {
Cursor blockCursor = getCursor().dropParentUntil(it -> it instanceof J.Block || it == Cursor.ROOT_VALUE);
if (!(blockCursor.getValue() instanceof J.Block)) {
return multiVariable;
}
VariableDeclarations mv = multiVariable;
if (blockCursor.getParent() != null && blockCursor.getParent().getValue() instanceof ClassDeclaration
&& multiVariable.getVariables().size() == 1
&& fieldName.equals(multiVariable.getVariables().get(0).getName().getSimpleName())) {
if (mv.getModifiers().stream().noneMatch(m -> m.getType() == J.Modifier.Type.Final)) {
Space prefix = Space.firstPrefix(mv.getVariables());
J.Modifier m = new J.Modifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null,
J.Modifier.Type.Final, Collections.emptyList());
if (mv.getModifiers().isEmpty()) {
mv = mv.withTypeExpression(mv.getTypeExpression().withPrefix(prefix));
} else {
m = m.withPrefix(prefix);
}
mv = mv.withModifiers(ListUtils.concat(mv.getModifiers(), m));
}
MethodDeclaration constructor = blockCursor.getParent().getMessage("applicableConstructor");
ClassDeclaration c = blockCursor.getParent().getValue();
TypeTree fieldType = TypeTree.build(fullyQualifiedName);
if (constructor == null) {
doAfterVisit(new AddConstructorVisitor(c.getSimpleName(), fieldName, fieldType));
} else {
doAfterVisit(new AddConstructorParameterAndAssignment(constructor, fieldName, fieldType));
}
}
return mv;
}
}
private static class AddConstructorVisitor extends JavaVisitor<ExecutionContext> {
private final String className;
private final String fieldName;
private final TypeTree type;
public AddConstructorVisitor(String className, String fieldName, TypeTree type) {
this.className = className;
this.fieldName = fieldName;
this.type = type;
}
@Override
public J visitBlock(Block block, ExecutionContext p) {
J result = (Block) super.visitBlock(block, p);
if (getCursor().getParent() != null) {
Object n = getCursor().getParent().getValue();
if (n instanceof ClassDeclaration) {
ClassDeclaration classDecl = (ClassDeclaration) n;
JavaType.FullyQualified typeFqn = TypeUtils.asFullyQualified(type.getType());
if (typeFqn != null && classDecl.getKind() == ClassDeclaration.Kind.Type.Class
&& className.equals(classDecl.getSimpleName())) {
JavaTemplate.Builder template = JavaTemplate.builder(""
+ classDecl.getSimpleName() + "(" + getFieldType(typeFqn) + " " + fieldName + ") {\n"
+ "this." + fieldName + " = " + fieldName + ";\n"
+ "}\n"
).contextSensitive();
FullyQualified fq = TypeUtils.asFullyQualified(type.getType());
if (fq != null) {
template.imports(fq.getFullyQualifiedName());
maybeAddImport(fq);
}
Optional<Statement> firstMethod = block.getStatements().stream()
.filter(MethodDeclaration.class::isInstance).findFirst();
return firstMethod
.map(statement -> (J) template.build().apply(getCursor(),
statement.getCoordinates().before()))
.orElseGet(() -> template.build().apply(getCursor(),
block.getCoordinates().lastStatement()));
}
}
}
return result;
}
}
private static class AddConstructorParameterAndAssignment extends JavaIsoVisitor<ExecutionContext> {
private final MethodDeclaration constructor;
private final String fieldName;
private final String methodType;
public AddConstructorParameterAndAssignment(MethodDeclaration constructor, String fieldName, TypeTree type) {
this.constructor = constructor;
this.fieldName = fieldName;
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(type.getType());
if (fq != null) {
methodType = getFieldType(fq);
} else {
throw new IllegalArgumentException("Unable to determine parameter type");
}
}
@Override
public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, ExecutionContext p) {
J.MethodDeclaration md = super.visitMethodDeclaration(method, p);
if (md == this.constructor && md.getBody() != null) {
List<Statement> newParams = new ArrayList<>(md.getParameters().stream().filter(s -> !(s instanceof J.Empty)).toList());
J.VariableDeclarations vd = new J.VariableDeclarations(
Tree.randomId(),
newParams.isEmpty() ? Space.EMPTY : Space.SINGLE_SPACE,
Markers.EMPTY,
Collections.emptyList(),
Collections.emptyList(),
TypeTree.build(methodType),
null,
Collections.emptyList(),
List.of(JRightPadded.build(new J.VariableDeclarations.NamedVariable(
Tree.randomId(),
Space.SINGLE_SPACE,
Markers.EMPTY,
createFieldNameIdentifier(),
Collections.emptyList(),
null,
null
)))
);
newParams.add(vd);
md = md.withParameters(newParams);
updateCursor(md);
// noinspection ConstantConditions
ShallowClass type = JavaType.ShallowClass.build(methodType);
J.FieldAccess fa = new J.FieldAccess(Tree.randomId(), Space.EMPTY, Markers.EMPTY, new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, Collections.emptyList(), "this", md.getMethodType().getDeclaringType(), null), JLeftPadded.build(createFieldNameIdentifier()), type);
Assignment assign = new J.Assignment(Tree.randomId(), Space.build("\n", Collections.emptyList()), Markers.EMPTY, fa, JLeftPadded.build(createFieldNameIdentifier()), type);
List<Statement> newStatements = new ArrayList<>(md.getBody().getStatements());
newStatements.add(assign);
md = md.withBody(autoFormat(md.getBody().withStatements(newStatements), p, getCursor()));
}
return md;
}
private J.Identifier createFieldNameIdentifier() {
return new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, Collections.emptyList(), fieldName, JavaType.ShallowClass.build(methodType), null);
}
}
private static String getFieldType(JavaType.FullyQualified fullyQualifiedType) {
if (fullyQualifiedType.getOwningClass() != null) {
String[] parts = fullyQualifiedType.getFullyQualifiedName().split("\\.");
if (parts.length < 2) {
return fullyQualifiedType.getClassName();
}
return parts[parts.length - 2] + "." + parts[parts.length - 1];
}
return fullyQualifiedType.getClassName();
}
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.java;
import java.util.ArrayList;
import java.util.List;
import org.openrewrite.NlsRewrite.Description;
import org.openrewrite.NlsRewrite.DisplayName;
import org.openrewrite.Recipe;
/**
* @author Udayani V
*/
public class InjectBeanCompletionRecipe extends Recipe {
@Override
public @DisplayName String getDisplayName() {
return "Inject bean completions";
}
@Override
public @Description String getDescription() {
return "Automates the injection of a specified bean into Spring components by adding the necessary field and import, creating the constructor if it doesn't exist, and injecting the bean as a constructor parameter.";
}
String fullyQualifiedName;
String fieldName;
String classFqName;
public InjectBeanCompletionRecipe(String fullyQualifiedName, String fieldName, String classFqName) {
this.fullyQualifiedName = fullyQualifiedName;
this.fieldName = fieldName;
this.classFqName = classFqName;
}
@Override
public List<Recipe> getRecipeList() {
List<Recipe> list = new ArrayList<>();
list.add(new AddFieldRecipe(fullyQualifiedName, classFqName));
list.add(new ConstructorInjectionRecipe(fullyQualifiedName, fieldName, classFqName));
return list;
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2021 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring;
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.properties.tree.Properties;
import org.openrewrite.test.RewriteTest;
import org.openrewrite.yaml.tree.Yaml;
import static org.assertj.core.api.Assertions.assertThat;
import static org.openrewrite.properties.Assertions.properties;
import static org.openrewrite.yaml.Assertions.yaml;
class CommentOutSpringPropertyKeyTest implements RewriteTest {
@DocumentExample
@Test
void yamlComment() {
rewriteRun(
spec -> spec.recipe(new CommentOutSpringPropertyKey("server.port", "This property has been removed.")),
//language=yaml
yaml(
"server.port: 8080",
"""
# This property has been removed.
# server.port: 8080
""")
);
}
@Test
void multipleYamlComments() {
rewriteRun(
spec -> spec.recipes(
new CommentOutSpringPropertyKey("test.propertyKey1", "my comment 1"),
new CommentOutSpringPropertyKey("test.propertyKey2", "my comment 2")
),
//language=yaml
yaml(
"""
test:
propertyKey1: xxx
propertyKey2: yyy
propertyKey3: zzz
""",
"""
test:
# my comment 2
# my comment 1
# propertyKey1: xxx
# propertyKey2: yyy
propertyKey3: zzz
""",
spec -> spec.path("application.yaml")
.afterRecipe(file ->
assertThat(
((Yaml.Mapping)
((Yaml.Mapping) file.getDocuments().get(0)
.getBlock()).getEntries().get(0)
.getValue()).getEntries().get(0)
.getPrefix())
.isEqualTo(
"""
# my comment 2
# my comment 1
# propertyKey1: xxx
# propertyKey2: yyy
\
"""
)
)
)
);
}
@Test
void multiplePropertiesComments() {
rewriteRun(
spec -> spec.recipes(
new CommentOutSpringPropertyKey("test.propertyKey1", "my comment 1"),
new CommentOutSpringPropertyKey("test.propertyKey2", "my comment 2")
),
//language=properties
properties(
"""
test.propertyKey1=xxx
test.propertyKey2=yyy
test.propertyKey3=zzz
""",
"""
# my comment 1
# test.propertyKey1=xxx
# my comment 2
# test.propertyKey2=yyy
test.propertyKey3=zzz
""",
spec -> spec.path("application.properties")
.afterRecipe(file ->
assertThat(((Properties.Comment) file.getContent().get(3)).getMessage())
.isEqualTo(" test.propertyKey2=yyy"))
)
);
}
}

View File

@@ -0,0 +1,332 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.java;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.RecipeRun;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
public class AddFieldRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new AddFieldRecipe("com.example.test.OwnerRepository", "com.example.demo.FooBar"))
.parser(JavaParser.fromJavaVersion()
.logCompilationWarningsAndErrors(true));
}
public static void runRecipeAndAssert(Recipe recipe, String beforeSourceStr, String expectedSourceStr, String dependsOn) {
JavaParser javaParser = JavaParser.fromJavaVersion().dependsOn(dependsOn).build();
List<SourceFile> list = javaParser.parse(beforeSourceStr).toList();
SourceFile beforeSource = list.get(0);
assertThat(beforeSource.printAll()).isEqualTo(beforeSourceStr);
InMemoryLargeSourceSet ss = new InMemoryLargeSourceSet(list);
RecipeRun recipeRun = recipe.run(ss, new InMemoryExecutionContext(t -> {
throw new RuntimeException(t);
}));
org.openrewrite.Result res = recipeRun.getChangeset().getAllResults().get(0);
assertThat(res.getAfter().printAll()).isEqualTo(expectedSourceStr);
}
// The test parses invalid LST and then applies the recipe
@Test
void addField() {
String beforeSourceStr = """
package com.example.demo;
class FooBar {
public void test() {
ownerR
}
}
""";
String expectedSourceStr = """
package com.example.demo;
class FooBar {
private final OwnerRepository ownerRepository;
public void test() {
ownerR
}
}
""";
String dependsOn = """
package com.example.demo;
public interface OwnerRepository{}
""";
Recipe recipe = new AddFieldRecipe("com.example.demo.OwnerRepository", "com.example.demo.FooBar");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void addFieldAndImport() {
String beforeSourceStr = """
package com.example.demo;
class FooBar {
public void test() {
ownerR
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
class FooBar {
private final OwnerRepository ownerRepository;
public void test() {
ownerR
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new AddFieldRecipe("com.example.test.OwnerRepository", "com.example.demo.FooBar");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void addNestedField() {
String beforeSourceStr = """
package com.example.demo;
class FooBar {
public void test() {
ownerR
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
class FooBar {
private final Inner.OwnerRepository ownerRepository;
public void test() {
ownerR
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new AddFieldRecipe("com.example.test.Inner.OwnerRepository", "com.example.demo.FooBar");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void addToNestedComponent() {
String beforeSourceStr = """
package com.example.demo;
class FooBar {
class Inner {
public void test() {
ownerR
}
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
class FooBar {
class Inner {
private final OwnerRepository ownerRepository;
public void test() {
ownerR
}
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new AddFieldRecipe("com.example.test.OwnerRepository", "com.example.demo.FooBar$Inner");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void addFieldToFirstClass() {
String beforeSourceStr = """
package com.example.demo;
class FooBar {
public void test() {
ownerR
}
}
class FooBarNew {
public void test1() {}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
class FooBar {
private final Inner.OwnerRepository ownerRepository;
public void test() {
ownerR
}
}
class FooBarNew {
public void test1() {}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new AddFieldRecipe("com.example.test.Inner.OwnerRepository", "com.example.demo.FooBar");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void addFieldToSecondClass() {
String beforeSourceStr = """
package com.example.demo;
import org.springframework.stereotype.Component;
@Component
class FooBar {
public void test() {
ownerR
}
}
@Component
class FooBarNew {
public void test1() {}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
import org.springframework.stereotype.Component;
@Component
class FooBar {
public void test() {
ownerR
}
}
@Component
class FooBarNew {
private final Inner.OwnerRepository ownerRepository;
public void test1() {}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new AddFieldRecipe("com.example.test.Inner.OwnerRepository", "com.example.demo.FooBarNew");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
}

View File

@@ -0,0 +1,489 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.java;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.RecipeRun;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
public class ConstructorInjectionRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A"))
.parser(JavaParser.fromJavaVersion().classpath("spring-beans"));
}
public static void runRecipeAndAssert(Recipe recipe, String beforeSourceStr, String expectedSourceStr, String dependsOn) {
JavaParser javaParser = JavaParser.fromJavaVersion().dependsOn(dependsOn).build();
List<SourceFile> list = javaParser.parse(beforeSourceStr).toList();
SourceFile beforeSource = list.get(0);
assertThat(beforeSource.printAll()).isEqualTo(beforeSourceStr);
InMemoryLargeSourceSet ss = new InMemoryLargeSourceSet(list);
RecipeRun recipeRun = recipe.run(ss, new InMemoryExecutionContext(t -> {
throw new RuntimeException(t);
}));
org.openrewrite.Result res = recipeRun.getChangeset().getAllResults().get(0);
assertThat(res.getAfter().printAll()).isEqualTo(expectedSourceStr);
}
@Test
void injectFieldIntoNewConstructor() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
private final OwnerRepository ownerRepository;
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
private final OwnerRepository ownerRepository;
A(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void injectFieldIntoExistingSingleConstructor() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
private final OwnerRepository ownerRepository;
A() {
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
private final OwnerRepository ownerRepository;
A(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void injectFieldIntoAutowiredConstructor() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
import org.springframework.beans.factory.annotation.Autowired;
public class A {
private final OwnerRepository ownerRepository;
@Autowired
A() {
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
import org.springframework.beans.factory.annotation.Autowired;
public class A {
private final OwnerRepository ownerRepository;
@Autowired
A(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void injectFieldIntoExistingConstructorWithFields() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
String a;
private final OwnerRepository ownerRepository;
A(String a) {
this.a = a;
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
String a;
private final OwnerRepository ownerRepository;
A(String a, OwnerRepository ownerRepository) {
this.a = a;
this.ownerRepository = ownerRepository;
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void injectInnerClassFieldIntoExistingConstructorWithFields() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
public class A {
String a;
private final Inner.OwnerRepository ownerRepository;
A(String a) {
this.a = a;
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
public class A {
String a;
private final Inner.OwnerRepository ownerRepository;
A(String a, Inner.OwnerRepository ownerRepository) {
this.a = a;
this.ownerRepository = ownerRepository;
}
}
""";
String dependsOn = """
package com.example.test;
public class Inner {
public static class OwnerRepository{}
}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.Inner.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void injectInnerClassFieldIntoNewConstructor() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
public class A {
private final Inner.OwnerRepository ownerRepository;
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.Inner.OwnerRepository;
public class A {
private final Inner.OwnerRepository ownerRepository;
A(Inner.OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
}
""";
String dependsOn = """
package com.example.test;
public class Inner {
public static class OwnerRepository{}
}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.Inner.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void nestedClass_InjectFieldIntoNewConstructor() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
public class Inner {
String a;
private final OwnerRepository ownerRepository;
public void test() {
}
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
public class Inner {
String a;
private final OwnerRepository ownerRepository;
Inner(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A$Inner");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void nestedClass_InjectFieldIntoExistingConstructorWithFields() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
public class Inner {
String a;
private final OwnerRepository ownerRepository;
Inner(String a) {
this.a = a;
}
public void test() {
}
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
public class Inner {
String a;
private final OwnerRepository ownerRepository;
Inner(String a, OwnerRepository ownerRepository) {
this.a = a;
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A$Inner");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
@Test
void nestedClass_InjectFieldIntoExistingSingleConstructor() {
String beforeSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
int param;
A(int param) {
this.param = param;
}
public class Inner {
private final OwnerRepository ownerRepository;
Inner() {
}
public void test() {
}
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
public class A {
int param;
A(int param) {
this.param = param;
}
public class Inner {
private final OwnerRepository ownerRepository;
Inner(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A$Inner");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
}

View File

@@ -0,0 +1,99 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.java;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.RecipeRun;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
public class InjectBeanCompletionRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A"))
.parser(JavaParser.fromJavaVersion().classpath("spring-beans"));
}
public static void runRecipeAndAssert(Recipe recipe, String beforeSourceStr, String expectedSourceStr, String dependsOn) {
JavaParser javaParser = JavaParser.fromJavaVersion().dependsOn(dependsOn).build();
List<SourceFile> list = javaParser.parse(beforeSourceStr).toList();
SourceFile beforeSource = list.get(0);
assertThat(beforeSource.printAll()).isEqualTo(beforeSourceStr);
InMemoryLargeSourceSet ss = new InMemoryLargeSourceSet(list);
RecipeRun recipeRun = recipe.run(ss, new InMemoryExecutionContext(t -> {
throw new RuntimeException(t);
}));
org.openrewrite.Result res = recipeRun.getChangeset().getAllResults().get(0);
assertThat(res.getAfter().printAll()).isEqualTo(expectedSourceStr);
}
@Test
void injectFieldIntoNewConstructor() {
String beforeSourceStr = """
package com.example.demo;
import org.springframework.stereotype.Controller;
@Controller
public class A {
public void test() {
}
}
""";
String expectedSourceStr = """
package com.example.demo;
import com.example.test.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class A {
private final OwnerRepository ownerRepository;
A(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
""";
String dependsOn = """
package com.example.test;
public interface OwnerRepository{}
""";
Recipe recipe = new InjectBeanCompletionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn);
}
}

View File

@@ -546,10 +546,23 @@ public class Editor {
}
}
Position beforeCmdCursorPosition = getCursor();
String beforeCmd = doc.getText();
// Apply command
if (completion.getCommand() != null) {
harness.executeCommand(completion.getCommand());
if (beforeCmd.length() != getRawText().length()) {
TextDocument beforeDoc = new TextDocument(doc.getUri(), doc.getLanguageId());
beforeDoc.setText(beforeCmd);
int beforeOffset = beforeDoc.toOffset(beforeCmdCursorPosition);
TextDocument changedDoc = new TextDocument(doc.getUri(), doc.getLanguageId());
changedDoc.setText(doc.getText());
int newOffset = beforeOffset + (getRawText().length() - beforeCmd.length());
setCursor(changedDoc.toPosition(newOffset));
}
}
}
private String getInsertText(CompletionItem completion) {

View File

@@ -909,8 +909,8 @@ public class LanguageServerHarness {
for (TextEdit textEdit : docEdit.getEdits()) {
Range range = textEdit.getRange();
edits.replace(workingDocument.toOffset(range.getStart()), workingDocument.toOffset(range.getEnd()), textEdit.getNewText());
edits.apply(workingDocument);
}
edits.apply(workingDocument);
Editor editor = getOpenEditor(uri);
if (editor!=null) {
editor.setRawText(workingDocument.get());

View File

@@ -131,6 +131,14 @@
<dependencyManagement>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openrewrite/rewrite-bom -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-bom</artifactId>
<version>8.42.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-recipe-bom</artifactId>

View File

@@ -26,6 +26,7 @@ import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeC
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.beans.BeanNamesCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.BeanTypesCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.BeanCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
@@ -38,6 +39,7 @@ import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionPro
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.boot.java.scope.ScopeCompletionProcessor;
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippet;
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext;
@@ -113,7 +115,8 @@ public class BootJavaCompletionEngineConfigurer {
@Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties,
JavaSnippetManager snippetManager,
CompilationUnitCache cuCache,
SpringMetamodelIndex springIndex) {
SpringMetamodelIndex springIndex,
RewriteRefactorings rewriteRefactorings ) {
SpringPropertyIndexProvider indexProvider = params.indexProvider;
JavaProjectFinder javaProjectFinder = params.projectFinder;
@@ -168,6 +171,8 @@ public class BootJavaCompletionEngineConfigurer {
providers.put(Annotations.SCHEDULED, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of(
"cron", new CronExpressionCompletionProvider())));
providers.put(Annotations.BEAN, new BeanCompletionProvider(javaProjectFinder, springIndex, rewriteRefactorings));
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
import org.springframework.ide.vscode.commons.rewrite.java.InjectBeanCompletionRecipe;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* @author Udayani V
* @author Alex Boyko
*/
public class BeanCompletionProposal implements ICompletionProposal {
private DocumentEdits edits;
private IDocument doc;
private String beanId;
private String beanType;
private String className;
private RewriteRefactorings rewriteRefactorings;
public BeanCompletionProposal(DocumentEdits edits, IDocument doc, String beanId, String beanType, String className,
RewriteRefactorings rewriteRefactorings) {
this.edits = edits;
this.doc = doc;
this.beanId = beanId;
this.beanType = beanType;
this.className = className;
this.rewriteRefactorings = rewriteRefactorings;
}
@Override
public String getLabel() {
return this.beanId;
}
@Override
public CompletionItemKind getKind() {
return CompletionItemKind.Constructor;
}
@Override
public DocumentEdits getTextEdit() {
return edits;
}
@Override
public String getDetail() {
return "Autowire a bean";
}
@Override
public Renderable getDocumentation() {
return Renderables.text(
"Inject bean `%s` of type `%s` as a constructor parameter and add corresponding field".formatted(beanId, beanType));
}
@Override
public Optional<Command> getCommand() {
FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()),"Inject bean completions")
.withParameters(Map.of("fullyQualifiedName", beanType, "fieldName", beanId, "classFqName", className))
.withRecipeScope(RecipeScope.NODE);
return Optional.of(rewriteRefactorings.createFixCommand("Inject bean '%s'".formatted(beanId), f));
}
}

View File

@@ -0,0 +1,143 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Collection;
import java.util.Optional;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* @author Udayani V
*/
public class BeanCompletionProvider implements CompletionProvider {
private static final Logger log = LoggerFactory.getLogger(BeanCompletionProvider.class);
private final JavaProjectFinder javaProjectFinder;
private final SpringMetamodelIndex springIndex;
private final RewriteRefactorings rewriteRefactorings;
public BeanCompletionProvider(JavaProjectFinder javaProjectFinder, SpringMetamodelIndex springIndex,
RewriteRefactorings rewriteRefactorings) {
this.javaProjectFinder = javaProjectFinder;
this.springIndex = springIndex;
this.rewriteRefactorings = rewriteRefactorings;
}
@Override
public void provideCompletions(ASTNode node, int offset, TextDocument doc,
Collection<ICompletionProposal> completions) {
if (node instanceof SimpleName) {
try {
// Don't look at anything inside Annotation or VariableDelcaration node
for (ASTNode n = node; n != null; n = n.getParent()) {
if (n instanceof Annotation
|| n instanceof VariableDeclaration) {
return;
}
}
Optional<IJavaProject> optionalProject = this.javaProjectFinder.find(doc.getId());
if (optionalProject.isEmpty()) {
return;
}
IJavaProject project = optionalProject.get();
TypeDeclaration topLevelClass = findParentClass(node);
if (topLevelClass == null) {
return;
}
if (isSpringComponent(topLevelClass)) {
String className = getFullyQualifiedName(topLevelClass);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
for (Bean bean : beans) {
if (FuzzyMatcher.matchScore(node.toString(), bean.getName()) != 0.0) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(offset - node.toString().length(), offset, bean.getName());
BeanCompletionProposal proposal = new BeanCompletionProposal(edits, doc, bean.getName(),
bean.getType(), className, rewriteRefactorings);
completions.add(proposal);
}
}
}
} catch (Exception e) {
log.error("problem while looking for bean completions", e);
}
}
}
private static boolean isSpringComponent(TypeDeclaration node) {
for (IAnnotationBinding annotation : node.resolveBinding().getAnnotations()) {
if (isSpringComponentAnnotation(annotation)) {
return true;
}
}
return false;
}
private static boolean isSpringComponentAnnotation(IAnnotationBinding annotation) {
String annotationName = annotation.getAnnotationType().getQualifiedName();
if (annotationName.equals("org.springframework.stereotype.Component")) {
return true;
}
for (IAnnotationBinding metaAnnotation : annotation.getAnnotationType().getAnnotations()) {
if (metaAnnotation.getAnnotationType().getQualifiedName().equals("org.springframework.stereotype.Component")) {
return true;
}
}
return false;
}
private static TypeDeclaration findParentClass(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof TypeDeclaration) {
return (TypeDeclaration) current;
}
current = current.getParent();
}
return null;
}
private static String getFullyQualifiedName(TypeDeclaration typeDecl) {
if (typeDecl.resolveBinding() != null) {
String qualifiedName = typeDecl.resolveBinding().getQualifiedName();
return qualifiedName.replaceAll("\\.(?=[^\\.]+$)", "\\$");
}
CompilationUnit cu = (CompilationUnit) typeDecl.getRoot();
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
String typeName = typeDecl.getName().getFullyQualifiedName();
return packageName.isEmpty() ? typeName : packageName + "." + typeName;
}
}

View File

@@ -15,6 +15,7 @@ import java.net.URI;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
@@ -22,11 +23,13 @@ import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.openrewrite.Parser.Input;
import org.openrewrite.Recipe;
import org.openrewrite.SourceFile;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.internal.RecipeIntrospectionUtils;
import org.openrewrite.java.JavaParser;
import org.openrewrite.marker.Range;
@@ -82,12 +85,23 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
@Override
public Mono<QuickfixEdit> createEdits(Object p) {
if (p instanceof JsonElement) {
return Mono.fromFuture(createEdit((JsonElement) p).thenApply(we -> new QuickfixEdit(we, null)));
if (p instanceof JsonElement je) {
return Mono.fromFuture(createEdit(je).thenApply(we -> new QuickfixEdit(we, null)));
} else {
return Mono.fromFuture(createEdit(gson.toJsonTree(p)).thenApply(we -> new QuickfixEdit(we, null)));
}
return null;
}
public Command createFixCommand(String title, FixDescriptor f) {
List<Object> args = new ArrayList<>(3);
args.add(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX);
args.add(gson.toJsonTree(f));
return new Command(
title,
server.CODE_ACTION_COMMAND_ID,
args
);
}
@Override
public CompletableFuture<WorkspaceEdit> resolve(CodeAction codeAction) {
@@ -154,16 +168,8 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
.orElseGet(() -> recipeRepo.getRecipe(d.getRecipeId()).thenApply(opt -> opt.orElseThrow())))
.thenApply(r -> {
if (d.getParameters() != null) {
for (Entry<String, Object> entry : d.getParameters().entrySet()) {
try {
Field f = r.getClass().getDeclaredField(entry.getKey());
f.setAccessible(true);
f.set(r, entry.getValue());
} catch (Exception e) {
log.error("", e);;
}
}
}
setParameters(r, d.getParameters());
}
if (d.getRecipeScope() == RecipeScope.NODE) {
if (d.getRangeScope() == null) {
throw new IllegalArgumentException("Missing scope AST node!");
@@ -187,4 +193,41 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
return r;
});
}
/**
* Sets the parameters for a given recipe. If the recipe is a DeclarativeRecipe,
* it iterates over its sub-recipes and sets the parameters for each sub-recipe.
*/
private void setParameters(Recipe recipe, Map<String, Object> parameters) {
if (recipe instanceof DeclarativeRecipe) {
List<Recipe> subRecipes = ((DeclarativeRecipe) recipe).getRecipeList();
for (Recipe subRecipe : subRecipes) {
setParameters(subRecipe, parameters);
}
} else {
for (Entry<String, Object> entry : parameters.entrySet()) {
try {
Field field = findField(recipe, entry.getKey());
if (field != null) {
field.setAccessible(true);
field.set(recipe, entry.getValue());
}
} catch (Exception e) {
log.error("", e);;
}
}
}
}
private Field findField(Object obj, String fieldName) {
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
}

View File

@@ -0,0 +1,362 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans.test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Udayani V
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class BeanCompletionProviderTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringMetamodelIndex springIndex;
@Autowired private SpringSymbolIndex indexer;
private File directory;
private IJavaProject project;
private Bean[] indexedBeans;
private String tempJavaDocUri;
private Bean bean1;
private Bean bean2;
private Bean bean3;
private Bean bean4;
private Bean bean5;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
String projectDir = directory.toURI().toString();
project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
indexedBeans = springIndex.getBeansOfProject(project.getElementName());
tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
bean1 = new Bean("ownerRepository", "org.springframework.samples.petclinic.owner.OwnerRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean2 = new Bean("ownerService", "org.springframework.samples.petclinic.owner.OwnerService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean3 = new Bean("visitRepository", "org.springframework.samples.petclinic.owner.VisitRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean4 = new Bean("visitService", "org.springframework.samples.petclinic.owner.VisitService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean5 = new Bean("petService", "org.springframework.samples.petclinic.pet.Inner.PetService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5});
}
@AfterEach
public void restoreIndexState() {
this.springIndex.updateBeans(project.getElementName(), indexedBeans);
}
@Test
public void testBeanCompletion_withMatches() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
ownerRepository<*>
}
}
""");
}
@Test
public void testBeanCompletion_withoutMatches() throws Exception {
assertCompletions(getCompletion("rand<*>"), new String[] {}, 0, "");
}
@Test
public void testBeanCompletion_chooseSecondCompletion() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 1,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerService ownerService;
TestBeanCompletionClass(OwnerService ownerService) {
this.ownerService = ownerService;
}
public void test() {
ownerService<*>
}
}
""");
}
@Test
public void testBeanCompletion_injectInnerClass() throws Exception {
assertCompletions(getCompletion("pet<*>"), new String[] {"petService"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.pet.Inner.PetService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final Inner.PetService petService;
TestBeanCompletionClass(Inner.PetService petService) {
this.petService = petService;
}
public void test() {
petService<*>
}
}
""");
}
@Test
public void testBeanCompletion_multipleClasses() throws Exception {
String content = """
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
@Controller
public class TestBeanCompletionSecondClass {
public void test() {
owner<*>
}
}
""";
assertCompletions(content, new String[] {"ownerRepository", "ownerService"}, 1,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.samples.petclinic.owner.OwnerService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
@Controller
public class TestBeanCompletionSecondClass {
private final OwnerService ownerService;
TestBeanCompletionSecondClass(OwnerService ownerService) {
this.ownerService = ownerService;
}
public void test() {
ownerService<*>
}
}
""");
}
@Test
public void testBeanCompletion_isNotSpringComponent() throws Exception {
String content = """
package org.sample.test;
public class TestBeanCompletionClass {
public void test() {
owner<*>
}
}
""";
// No suggestions when it is not a spring component
assertCompletions(content, new String[] {}, 0, "");
}
@Test
public void testBeanCompletion_isOutsideMethod() throws Exception {
String content = """
package org.sample.test;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
owner<*>
}
""";
assertCompletions(content, new String[] {}, 0, "");
}
@Test
public void testBeanCompletion_nestedComponent() throws Exception {
String content = """
package org.sample.test;
import org.springframework.stereotype.Component;
@Component
public class TestBeanCompletionClass {
@Component
public class Inner {
public void test() {
ownerRe<*>
}
}
}
""";
assertCompletions(content, new String[] {"ownerRepository"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Component;
@Component
public class TestBeanCompletionClass {
@Component
public class Inner {
private final OwnerRepository ownerRepository;
Inner(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
ownerRepository<*>
}
}
}
""");
}
private String getCompletion(String completionLine) {
String content = """
package org.sample.test;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
public void test() {
""" +
completionLine + "\n" +
"""
}
}
""";
return content;
}
private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedResult);
}
private void assertCompletions(String editorContent, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
List<CompletionItem> completions = editor.getCompletions();
assertEquals(noOfExcpectedCompletions, completions.size());
if (expectedCompletions != null) {
String[] completionItems = completions.stream()
.map(item -> item.getLabel())
.toArray(size -> new String[size]);
assertArrayEquals(expectedCompletions, completionItems);
}
if (noOfExcpectedCompletions > 0) {
editor.apply(completions.get(chosenCompletion));
assertEquals(expectedResult, editor.getText());
}
}
}