From edd78c0307b6d312f462e2b96eb2e85351e841c9 Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 3 Mar 2025 15:01:14 -0500 Subject: [PATCH] Bean completion case of constructor with parameter --- .../java/ConstructorInjectionRecipe.java | 68 +++++++++++-------- .../java/ConstructorInjectionRecipeTest.java | 43 ++++++++++++ .../test/BeanCompletionProviderTest.java | 62 ++++++++++++++--- 3 files changed, 137 insertions(+), 36 deletions(-) diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java index ef729c852..1181538f7 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java @@ -139,13 +139,16 @@ public class ConstructorInjectionRecipe extends Recipe { public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { - Cursor blockCursor = getCursor().dropParentUntil(it -> it instanceof J.Block || it == Cursor.ROOT_VALUE); + Cursor blockCursor = getCursor().getParentTreeCursor(); if (!(blockCursor.getValue() instanceof J.Block)) { return multiVariable; } + Cursor typeDeclCursor = blockCursor.getParentTreeCursor(); + if (!(typeDeclCursor.getValue() instanceof J.ClassDeclaration)) { + return multiVariable; + } VariableDeclarations mv = multiVariable; - if (blockCursor.getParent() != null && blockCursor.getParent().getValue() instanceof ClassDeclaration - && multiVariable.getVariables().size() == 1 + if (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()); @@ -236,31 +239,42 @@ public class ConstructorInjectionRecipe extends Recipe { @Override public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, ExecutionContext p) { J.MethodDeclaration md = super.visitMethodDeclaration(method, p); - if (md == this.constructor && md.getBody() != null) { + if (md.getId().equals(constructor.getId()) && md.getBody() != null) { - List 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); + boolean parameterExists = md.getParameters().stream().filter(J.VariableDeclarations.class::isInstance).map(J.VariableDeclarations.class::cast).filter(vd -> { + if (vd.getVariables().stream().anyMatch(vn -> fieldName.equals(vn.getSimpleName()))) { + FullyQualified fqType = vd.getTypeAsFullyQualified(); + if (fqType != null && methodType.equals(fqType.getClassName())) { + return true; + } + } + return false; + }).findFirst().isPresent(); + if (!parameterExists) { + List 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); + } if (!isConstructorInitializingField(md, fieldName)) { // noinspection ConstantConditions diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java index 862cb936b..9160f1ad8 100644 --- a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java +++ b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java @@ -136,6 +136,49 @@ public class ConstructorInjectionRecipeTest implements RewriteTest { runRecipeAndAssert(recipe, beforeSourceStr, expectedSourceStr, dependsOn); } + @Test + void injectFieldIntoExistingSingleConstructorWithExisitngParameter() { + + String beforeSourceStr = """ + package com.example.demo; + + import com.example.test.OwnerRepository; + + public class A { + + private final OwnerRepository ownerRepository; + + A(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 injectFieldIntoAutowiredConstructor() { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java index caf0be4d8..e9e8749d9 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java @@ -69,6 +69,7 @@ public class BeanCompletionProviderTest { private Bean bean4; private Bean bean5; private Bean bean6; + private Bean bean7; @BeforeEach public void setup() throws Exception { @@ -94,8 +95,9 @@ public class BeanCompletionProviderTest { 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, "symbolLabel"); 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, "symbolLabel"); bean6 = new Bean("testBeanCompletionClass", "org.sample.test.TestBeanCompletionClass", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false, "symbolLabel"); + bean7 = new Bean("testIntBean", "java.lang.Integer", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false, "symbolLabel"); - springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5, bean6}); + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5, bean6, bean7}); } @AfterEach @@ -155,7 +157,7 @@ ownerService<*> @Test public void noPrefix_secondCompletion() throws Exception { - assertCompletions(getCompletion("<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 1, + assertCompletions(getCompletion("<*>"), new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 1, """ package org.sample.test; @@ -180,7 +182,7 @@ ownerService<*> @Test public void testBeanCompletion_injectInnerClass() throws Exception { - assertCompletions(getCompletion("<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 2, + assertCompletions(getCompletion("<*>"), new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 2, """ package org.sample.test; @@ -297,7 +299,7 @@ public class TestBeanCompletionSecondClass { } """; - assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testBeanCompletionClass", "visitRepository", "visitService"}, 1, + assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testBeanCompletionClass", "testIntBean", "visitRepository", "visitService"}, 1, """ package org.sample.test; @@ -526,6 +528,48 @@ public class TestBeanCompletionClass { """); } + @Test + public void beanCompletionWithConstructorParameterPresent() throws Exception { + String content = """ + package org.sample.test; + + import org.springframework.stereotype.Controller; + + @Controller + public class TestBeanCompletionClass { + + TestBeanCompletionClass(Integer testIntBean) { + } + + public void test() { + this.testI<*> + } + } + """; + + + assertCompletions(content, new String[] {"testIntBean"}, 0, + """ + package org.sample.test; + + import org.springframework.stereotype.Controller; + + @Controller + public class TestBeanCompletionClass { + + private final Integer testIntBean; + + TestBeanCompletionClass(Integer testIntBean) { + this.testIntBean = testIntBean; + } + + public void test() { + this.testIntBean<*> + } + } + """); + } + @Test public void noCompletionsInMethod_1() throws Exception { String content = """ @@ -659,7 +703,7 @@ public class TestBeanCompletionClass { """; - assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0, + assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 0, """ package org.sample.test; @@ -965,7 +1009,7 @@ public class TestBeanCompletionClass { """; - assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0, + assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 0, """ package org.sample.test; @@ -1002,7 +1046,7 @@ public class TestBeanCompletionClass { """; - assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0, + assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 0, """ package org.sample.test; @@ -1039,7 +1083,7 @@ public class TestBeanCompletionClass { """; - assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0, + assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 0, """ package org.sample.test; @@ -1076,7 +1120,7 @@ public class TestBeanCompletionClass { """; - assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0, + assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testIntBean", "visitRepository", "visitService"}, 0, """ package org.sample.test;