Skip to content

Commit

Permalink
Bean completion inside constructors
Browse files Browse the repository at this point in the history
  • Loading branch information
BoykoAlex committed Feb 28, 2025
1 parent bccd2ba commit 1238fec
Show file tree
Hide file tree
Showing 5 changed files with 563 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import org.jspecify.annotations.NonNull;
Expand All @@ -34,6 +35,7 @@
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.Identifier;
import org.openrewrite.java.tree.J.MethodDeclaration;
import org.openrewrite.java.tree.J.VariableDeclarations;
import org.openrewrite.java.tree.JLeftPadded;
Expand Down Expand Up @@ -113,7 +115,7 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct
} else if (constructors.size() == 1) {
MethodDeclaration c = constructors.get(0);
getCursor().putMessage("applicableConstructor", c);
applicable = isNotConstructorInitializingField(c, fieldName);
applicable = !isConstructorInitializingField(c, fieldName);
} else {
List<MethodDeclaration> autowiredConstructors = constructors.stream()
.filter(constr -> constr.getLeadingAnnotations().stream()
Expand All @@ -123,7 +125,7 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct
if (autowiredConstructors.size() == 1) {
MethodDeclaration c = autowiredConstructors.get(0);
getCursor().putMessage("applicableConstructor", autowiredConstructors.get(0));
applicable = isNotConstructorInitializingField(c, fieldName);
applicable = !isConstructorInitializingField(c, fieldName);
}
}
if (applicable) {
Expand All @@ -133,31 +135,6 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct
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) {
Expand Down Expand Up @@ -285,20 +262,22 @@ public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, Execut
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);
assign = autoFormat(assign, p, getCursor());
List<Statement> newStatements = new ArrayList<>(md.getBody().getStatements());
boolean empty = newStatements.isEmpty();
if (empty) {
newStatements.add(assign);
md = md.withBody(autoFormat(md.getBody().withStatements(newStatements), p, getCursor()));
} else {
// Prefix is off otherwise even after autoFormat
newStatements.add(assign.withPrefix(newStatements.get(newStatements.size() - 1).getPrefix()));
md = md.withBody(md.getBody().withStatements(newStatements));
if (!isConstructorInitializingField(md, fieldName)) {
// 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);
assign = autoFormat(assign, p, getCursor());
List<Statement> newStatements = new ArrayList<>(md.getBody().getStatements());
boolean empty = newStatements.isEmpty();
if (empty) {
newStatements.add(assign);
md = md.withBody(autoFormat(md.getBody().withStatements(newStatements), p, getCursor()));
} else {
// Prefix is off otherwise even after autoFormat
newStatements.add(assign.withPrefix(newStatements.get(newStatements.size() - 1).getPrefix()));
md = md.withBody(md.getBody().withStatements(newStatements));
}
}
}
return md;
Expand All @@ -321,4 +300,48 @@ private static String getFieldType(JavaType.FullyQualified fullyQualifiedType) {

return fullyQualifiedType.getClassName();
}

private static boolean isConstructorInitializingField(MethodDeclaration c, String fieldName) {
AtomicBoolean res = new AtomicBoolean();
new JavaIsoVisitor<AtomicBoolean>() {

@Override
public Assignment visitAssignment(Assignment assignment, AtomicBoolean ab) {
if (ab.get() || getCursor().firstEnclosing(MethodDeclaration.class) != c) {
return assignment;
}
Assignment a = super.visitAssignment(assignment, ab);
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())) {
ab.set(true);
return a;
}
}
}
return a;
}

@Override
public Identifier visitIdentifier(Identifier identifier, AtomicBoolean ab) {
if (ab.get() || getCursor().firstEnclosing(MethodDeclaration.class) != c) {
return identifier;
}
Identifier id = super.visitIdentifier(identifier, ab);
JavaType.Variable fieldType = c.getMethodType().getDeclaringType().getMembers().stream()
.filter(v -> fieldName.equals(v.getName())).findFirst().orElse(null);
if (fieldType != null && fieldType.equals(id.getFieldType())) {
ab.set(true);
}
return id;
}
}.visit(c, res);
return res.get();
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,28 @@
import java.util.Map;
import java.util.Optional;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.CompletionItemLabelDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
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.ICompletionProposalWithScore;
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.BadLocationException;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
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;
Expand All @@ -32,27 +45,35 @@
* @author Alex Boyko
*/
public class BeanCompletionProposal implements ICompletionProposalWithScore {


private static final Logger log = LoggerFactory.getLogger(BeanCompletionProposal.class);

private static final String SHORT_DESCRIPTION = "inject as a bean dependency";

private DocumentEdits edits;

private IDocument doc;
private String beanId;
private String beanType;
private String className;
private RewriteRefactorings rewriteRefactorings;
private double score;
private ASTNode node;
private int offset;

public BeanCompletionProposal(DocumentEdits edits, IDocument doc, String beanId, String beanType, String className,
double score,
RewriteRefactorings rewriteRefactorings) {
this.edits = edits;
private String prefix;
private DocumentEdits edits;

public BeanCompletionProposal(ASTNode node, int offset, IDocument doc, String beanId, String beanType,
String className, RewriteRefactorings rewriteRefactorings) {
this.node = node;
this.offset = offset;
this.doc = doc;
this.beanId = beanId;
this.beanType = beanType;
this.className = className;
this.score = score;
this.rewriteRefactorings = rewriteRefactorings;
this.prefix = computePrefix();
this.edits = computeEdit();
this.score = FuzzyMatcher.matchScore(prefix, beanId);
}

@Override
Expand All @@ -65,6 +86,67 @@ public CompletionItemKind getKind() {
return CompletionItemKind.Field;
}

private String computePrefix() {
String prefix = "";
try {
// Empty SimpleName usually comes from unresolved FieldAccess, i.e. `this.owner`
// where `owner` field is not defined
if (node instanceof SimpleName sn) {
FieldAccess fa = getFieldAccessFromIncompleteThisAssignment(sn);
if (fa != null) {
prefix = fa.getName().toString();
} else if (!BootJavaCompletionEngine.$MISSING$.equals(sn.toString())) {
prefix = sn.toString();
}
} else if (isIncompleteThisFieldAccess()) {
FieldAccess fa = (FieldAccess) node;
int start = fa.getExpression().getStartPosition() + fa.getExpression().getLength();
while (start < doc.getLength() && doc.getChar(start) != '.') {
start++;
}
prefix = doc.get(start + 1, offset - start - 1);
}
} catch (BadLocationException e) {
log.error("Failed to compute prefix for completion proposal", e);
}
return prefix;
}

private boolean isIncompleteThisFieldAccess() {
return node instanceof FieldAccess fa && fa.getExpression() instanceof ThisExpression;
}

private FieldAccess getFieldAccessFromIncompleteThisAssignment(SimpleName sn) {
if ((node.getLength() == 0 || BootJavaCompletionEngine.$MISSING$.equals(sn.toString()))
&& sn.getParent() instanceof Assignment assign && assign.getLeftHandSide() instanceof FieldAccess fa
&& fa.getExpression() instanceof ThisExpression) {
return fa;
}
return null;
}

private DocumentEdits computeEdit() {
DocumentEdits edits = new DocumentEdits(doc, false);
if (isInsideConstructor(node)) {
if (node instanceof Block) {
edits.insert(offset, "this.%s = %s;".formatted(beanId, beanId));
} else {
if (node.getParent() instanceof Assignment || node.getParent() instanceof FieldAccess) {
edits.replace(offset - prefix.length(), offset, "%s = %s;".formatted(beanId, beanId));
} else {
edits.replace(offset - prefix.length(), offset, "this.%s = %s;".formatted(beanId, beanId));
}
}
} else {
if (node instanceof Block) {
edits.insert(offset, beanId);
} else {
edits.replace(offset - prefix.length(), offset, beanId);
}
}
return edits;
}

@Override
public DocumentEdits getTextEdit() {
return edits;
Expand All @@ -74,7 +156,7 @@ public DocumentEdits getTextEdit() {
public String getDetail() {
return "Autowire a bean";
}

@Override
public CompletionItemLabelDetails getLabelDetails() {
CompletionItemLabelDetails labelDetails = new CompletionItemLabelDetails();
Expand All @@ -84,13 +166,14 @@ public CompletionItemLabelDetails getLabelDetails() {

@Override
public Renderable getDocumentation() {
return Renderables.text(
"Inject bean `%s` of type `%s` as a constructor parameter and add corresponding field".formatted(beanId, beanType));
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")
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));
Expand All @@ -100,5 +183,14 @@ public Optional<Command> getCommand() {
public double getScore() {
return score;
}


private boolean isInsideConstructor(ASTNode node) {
for (ASTNode n = node; n != null && !(n instanceof CompilationUnit); n = n.getParent()) {
if (n instanceof MethodDeclaration md) {
return md.isConstructor() || md.isCompactConstructor();
}
}
return false;
}

}
Loading

0 comments on commit 1238fec

Please sign in to comment.