Skip to content

Commit 1238fec

Browse files
committed
Bean completion inside constructors
1 parent bccd2ba commit 1238fec

File tree

5 files changed

+563
-91
lines changed

5 files changed

+563
-91
lines changed

headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.List;
1616
import java.util.Objects;
1717
import java.util.Optional;
18+
import java.util.concurrent.atomic.AtomicBoolean;
1819
import java.util.stream.Collectors;
1920

2021
import org.jspecify.annotations.NonNull;
@@ -34,6 +35,7 @@
3435
import org.openrewrite.java.tree.J.Assignment;
3536
import org.openrewrite.java.tree.J.Block;
3637
import org.openrewrite.java.tree.J.ClassDeclaration;
38+
import org.openrewrite.java.tree.J.Identifier;
3739
import org.openrewrite.java.tree.J.MethodDeclaration;
3840
import org.openrewrite.java.tree.J.VariableDeclarations;
3941
import org.openrewrite.java.tree.JLeftPadded;
@@ -113,7 +115,7 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct
113115
} else if (constructors.size() == 1) {
114116
MethodDeclaration c = constructors.get(0);
115117
getCursor().putMessage("applicableConstructor", c);
116-
applicable = isNotConstructorInitializingField(c, fieldName);
118+
applicable = !isConstructorInitializingField(c, fieldName);
117119
} else {
118120
List<MethodDeclaration> autowiredConstructors = constructors.stream()
119121
.filter(constr -> constr.getLeadingAnnotations().stream()
@@ -123,7 +125,7 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct
123125
if (autowiredConstructors.size() == 1) {
124126
MethodDeclaration c = autowiredConstructors.get(0);
125127
getCursor().putMessage("applicableConstructor", autowiredConstructors.get(0));
126-
applicable = isNotConstructorInitializingField(c, fieldName);
128+
applicable = !isConstructorInitializingField(c, fieldName);
127129
}
128130
}
129131
if (applicable) {
@@ -133,31 +135,6 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct
133135
return super.visitClassDeclaration(classDecl, ctx);
134136
}
135137

136-
public static boolean isNotConstructorInitializingField(MethodDeclaration c, String fieldName) {
137-
return c.getBody() == null || c.getBody().getStatements().stream().filter(J.Assignment.class::isInstance)
138-
.map(J.Assignment.class::cast).noneMatch(a -> {
139-
Expression expr = a.getVariable();
140-
if (expr instanceof J.FieldAccess) {
141-
J.FieldAccess fa = (J.FieldAccess) expr;
142-
if (fieldName.equals(fa.getSimpleName()) && fa.getTarget() instanceof J.Identifier) {
143-
J.Identifier target = (J.Identifier) fa.getTarget();
144-
if ("this".equals(target.getSimpleName())) {
145-
return true;
146-
}
147-
}
148-
}
149-
if (expr instanceof J.Identifier) {
150-
JavaType.Variable fieldType = c.getMethodType().getDeclaringType().getMembers().stream()
151-
.filter(v -> fieldName.equals(v.getName())).findFirst().orElse(null);
152-
if (fieldType != null) {
153-
J.Identifier identifier = (J.Identifier) expr;
154-
return fieldType.equals(identifier.getFieldType());
155-
}
156-
}
157-
return false;
158-
});
159-
}
160-
161138
@Override
162139
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable,
163140
ExecutionContext ctx) {
@@ -285,20 +262,22 @@ public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, Execut
285262
md = md.withParameters(newParams);
286263
updateCursor(md);
287264

288-
// noinspection ConstantConditions
289-
ShallowClass type = JavaType.ShallowClass.build(methodType);
290-
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);
291-
Assignment assign = new J.Assignment(Tree.randomId(), Space.build("\n", Collections.emptyList()), Markers.EMPTY, fa, JLeftPadded.build(createFieldNameIdentifier()), type);
292-
assign = autoFormat(assign, p, getCursor());
293-
List<Statement> newStatements = new ArrayList<>(md.getBody().getStatements());
294-
boolean empty = newStatements.isEmpty();
295-
if (empty) {
296-
newStatements.add(assign);
297-
md = md.withBody(autoFormat(md.getBody().withStatements(newStatements), p, getCursor()));
298-
} else {
299-
// Prefix is off otherwise even after autoFormat
300-
newStatements.add(assign.withPrefix(newStatements.get(newStatements.size() - 1).getPrefix()));
301-
md = md.withBody(md.getBody().withStatements(newStatements));
265+
if (!isConstructorInitializingField(md, fieldName)) {
266+
// noinspection ConstantConditions
267+
ShallowClass type = JavaType.ShallowClass.build(methodType);
268+
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);
269+
Assignment assign = new J.Assignment(Tree.randomId(), Space.build("\n", Collections.emptyList()), Markers.EMPTY, fa, JLeftPadded.build(createFieldNameIdentifier()), type);
270+
assign = autoFormat(assign, p, getCursor());
271+
List<Statement> newStatements = new ArrayList<>(md.getBody().getStatements());
272+
boolean empty = newStatements.isEmpty();
273+
if (empty) {
274+
newStatements.add(assign);
275+
md = md.withBody(autoFormat(md.getBody().withStatements(newStatements), p, getCursor()));
276+
} else {
277+
// Prefix is off otherwise even after autoFormat
278+
newStatements.add(assign.withPrefix(newStatements.get(newStatements.size() - 1).getPrefix()));
279+
md = md.withBody(md.getBody().withStatements(newStatements));
280+
}
302281
}
303282
}
304283
return md;
@@ -321,4 +300,48 @@ private static String getFieldType(JavaType.FullyQualified fullyQualifiedType) {
321300

322301
return fullyQualifiedType.getClassName();
323302
}
303+
304+
private static boolean isConstructorInitializingField(MethodDeclaration c, String fieldName) {
305+
AtomicBoolean res = new AtomicBoolean();
306+
new JavaIsoVisitor<AtomicBoolean>() {
307+
308+
@Override
309+
public Assignment visitAssignment(Assignment assignment, AtomicBoolean ab) {
310+
if (ab.get() || getCursor().firstEnclosing(MethodDeclaration.class) != c) {
311+
return assignment;
312+
}
313+
Assignment a = super.visitAssignment(assignment, ab);
314+
Expression expr = a.getVariable();
315+
if (expr instanceof J.FieldAccess) {
316+
J.FieldAccess fa = (J.FieldAccess) expr;
317+
if (fieldName.equals(fa.getSimpleName()) && fa.getTarget() instanceof J.Identifier) {
318+
J.Identifier target = (J.Identifier) fa.getTarget();
319+
if ("this".equals(target.getSimpleName())) {
320+
ab.set(true);
321+
return a;
322+
}
323+
}
324+
}
325+
return a;
326+
}
327+
328+
@Override
329+
public Identifier visitIdentifier(Identifier identifier, AtomicBoolean ab) {
330+
if (ab.get() || getCursor().firstEnclosing(MethodDeclaration.class) != c) {
331+
return identifier;
332+
}
333+
Identifier id = super.visitIdentifier(identifier, ab);
334+
JavaType.Variable fieldType = c.getMethodType().getDeclaringType().getMembers().stream()
335+
.filter(v -> fieldName.equals(v.getName())).findFirst().orElse(null);
336+
if (fieldType != null && fieldType.equals(id.getFieldType())) {
337+
ab.set(true);
338+
}
339+
return id;
340+
}
341+
}.visit(c, res);
342+
return res.get();
343+
}
344+
345+
346+
324347
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,28 @@
1414
import java.util.Map;
1515
import java.util.Optional;
1616

17+
import org.eclipse.jdt.core.dom.ASTNode;
18+
import org.eclipse.jdt.core.dom.Assignment;
19+
import org.eclipse.jdt.core.dom.Block;
20+
import org.eclipse.jdt.core.dom.CompilationUnit;
21+
import org.eclipse.jdt.core.dom.FieldAccess;
22+
import org.eclipse.jdt.core.dom.MethodDeclaration;
23+
import org.eclipse.jdt.core.dom.SimpleName;
24+
import org.eclipse.jdt.core.dom.ThisExpression;
1725
import org.eclipse.lsp4j.Command;
1826
import org.eclipse.lsp4j.CompletionItemKind;
1927
import org.eclipse.lsp4j.CompletionItemLabelDetails;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
2031
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
2132
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
2233
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposalWithScore;
2334
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
2435
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
2536
import org.springframework.ide.vscode.commons.rewrite.java.InjectBeanCompletionRecipe;
37+
import org.springframework.ide.vscode.commons.util.BadLocationException;
38+
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
2639
import org.springframework.ide.vscode.commons.util.Renderable;
2740
import org.springframework.ide.vscode.commons.util.Renderables;
2841
import org.springframework.ide.vscode.commons.util.text.IDocument;
@@ -32,27 +45,35 @@
3245
* @author Alex Boyko
3346
*/
3447
public class BeanCompletionProposal implements ICompletionProposalWithScore {
35-
48+
49+
private static final Logger log = LoggerFactory.getLogger(BeanCompletionProposal.class);
50+
3651
private static final String SHORT_DESCRIPTION = "inject as a bean dependency";
37-
38-
private DocumentEdits edits;
52+
3953
private IDocument doc;
4054
private String beanId;
4155
private String beanType;
4256
private String className;
4357
private RewriteRefactorings rewriteRefactorings;
4458
private double score;
59+
private ASTNode node;
60+
private int offset;
4561

46-
public BeanCompletionProposal(DocumentEdits edits, IDocument doc, String beanId, String beanType, String className,
47-
double score,
48-
RewriteRefactorings rewriteRefactorings) {
49-
this.edits = edits;
62+
private String prefix;
63+
private DocumentEdits edits;
64+
65+
public BeanCompletionProposal(ASTNode node, int offset, IDocument doc, String beanId, String beanType,
66+
String className, RewriteRefactorings rewriteRefactorings) {
67+
this.node = node;
68+
this.offset = offset;
5069
this.doc = doc;
5170
this.beanId = beanId;
5271
this.beanType = beanType;
5372
this.className = className;
54-
this.score = score;
5573
this.rewriteRefactorings = rewriteRefactorings;
74+
this.prefix = computePrefix();
75+
this.edits = computeEdit();
76+
this.score = FuzzyMatcher.matchScore(prefix, beanId);
5677
}
5778

5879
@Override
@@ -65,6 +86,67 @@ public CompletionItemKind getKind() {
6586
return CompletionItemKind.Field;
6687
}
6788

89+
private String computePrefix() {
90+
String prefix = "";
91+
try {
92+
// Empty SimpleName usually comes from unresolved FieldAccess, i.e. `this.owner`
93+
// where `owner` field is not defined
94+
if (node instanceof SimpleName sn) {
95+
FieldAccess fa = getFieldAccessFromIncompleteThisAssignment(sn);
96+
if (fa != null) {
97+
prefix = fa.getName().toString();
98+
} else if (!BootJavaCompletionEngine.$MISSING$.equals(sn.toString())) {
99+
prefix = sn.toString();
100+
}
101+
} else if (isIncompleteThisFieldAccess()) {
102+
FieldAccess fa = (FieldAccess) node;
103+
int start = fa.getExpression().getStartPosition() + fa.getExpression().getLength();
104+
while (start < doc.getLength() && doc.getChar(start) != '.') {
105+
start++;
106+
}
107+
prefix = doc.get(start + 1, offset - start - 1);
108+
}
109+
} catch (BadLocationException e) {
110+
log.error("Failed to compute prefix for completion proposal", e);
111+
}
112+
return prefix;
113+
}
114+
115+
private boolean isIncompleteThisFieldAccess() {
116+
return node instanceof FieldAccess fa && fa.getExpression() instanceof ThisExpression;
117+
}
118+
119+
private FieldAccess getFieldAccessFromIncompleteThisAssignment(SimpleName sn) {
120+
if ((node.getLength() == 0 || BootJavaCompletionEngine.$MISSING$.equals(sn.toString()))
121+
&& sn.getParent() instanceof Assignment assign && assign.getLeftHandSide() instanceof FieldAccess fa
122+
&& fa.getExpression() instanceof ThisExpression) {
123+
return fa;
124+
}
125+
return null;
126+
}
127+
128+
private DocumentEdits computeEdit() {
129+
DocumentEdits edits = new DocumentEdits(doc, false);
130+
if (isInsideConstructor(node)) {
131+
if (node instanceof Block) {
132+
edits.insert(offset, "this.%s = %s;".formatted(beanId, beanId));
133+
} else {
134+
if (node.getParent() instanceof Assignment || node.getParent() instanceof FieldAccess) {
135+
edits.replace(offset - prefix.length(), offset, "%s = %s;".formatted(beanId, beanId));
136+
} else {
137+
edits.replace(offset - prefix.length(), offset, "this.%s = %s;".formatted(beanId, beanId));
138+
}
139+
}
140+
} else {
141+
if (node instanceof Block) {
142+
edits.insert(offset, beanId);
143+
} else {
144+
edits.replace(offset - prefix.length(), offset, beanId);
145+
}
146+
}
147+
return edits;
148+
}
149+
68150
@Override
69151
public DocumentEdits getTextEdit() {
70152
return edits;
@@ -74,7 +156,7 @@ public DocumentEdits getTextEdit() {
74156
public String getDetail() {
75157
return "Autowire a bean";
76158
}
77-
159+
78160
@Override
79161
public CompletionItemLabelDetails getLabelDetails() {
80162
CompletionItemLabelDetails labelDetails = new CompletionItemLabelDetails();
@@ -84,13 +166,14 @@ public CompletionItemLabelDetails getLabelDetails() {
84166

85167
@Override
86168
public Renderable getDocumentation() {
87-
return Renderables.text(
88-
"Inject bean `%s` of type `%s` as a constructor parameter and add corresponding field".formatted(beanId, beanType));
169+
return Renderables.text("Inject bean `%s` of type `%s` as a constructor parameter and add corresponding field"
170+
.formatted(beanId, beanType));
89171
}
90172

91173
@Override
92174
public Optional<Command> getCommand() {
93-
FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()),"Inject bean completions")
175+
FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()),
176+
"Inject bean completions")
94177
.withParameters(Map.of("fullyQualifiedName", beanType, "fieldName", beanId, "classFqName", className))
95178
.withRecipeScope(RecipeScope.NODE);
96179
return Optional.of(rewriteRefactorings.createFixCommand("Inject bean '%s'".formatted(beanId), f));
@@ -100,5 +183,14 @@ public Optional<Command> getCommand() {
100183
public double getScore() {
101184
return score;
102185
}
103-
186+
187+
private boolean isInsideConstructor(ASTNode node) {
188+
for (ASTNode n = node; n != null && !(n instanceof CompilationUnit); n = n.getParent()) {
189+
if (n instanceof MethodDeclaration md) {
190+
return md.isConstructor() || md.isCompactConstructor();
191+
}
192+
}
193+
return false;
194+
}
195+
104196
}

0 commit comments

Comments
 (0)