Browse Source

Add `LocalVariableNameFactory`.

Add a variable name factory that considers predefined names and resolves name clashes.
Expose variable name clash resolution via the generation context of a single method.

Closes #3270
Original pull request: #3271
pull/3304/head
Christoph Strobl 8 months ago committed by Mark Paluch
parent
commit
6360ef83b4
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 37
      src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
  2. 24
      src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java
  3. 87
      src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java
  4. 48
      src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java
  5. 38
      src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java
  6. 70
      src/test/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContextUnitTests.java
  7. 72
      src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java
  8. 85
      src/test/java/org/springframework/data/repository/aot/generate/MethodMetadataUnitTests.java

37
src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java

@ -19,12 +19,10 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map.Entry;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationSelectors; import org.springframework.core.annotation.MergedAnnotationSelectors;
@ -54,6 +52,7 @@ public class AotQueryMethodGenerationContext {
private final AotRepositoryFragmentMetadata targetTypeMetadata; private final AotRepositoryFragmentMetadata targetTypeMetadata;
private final MethodMetadata targetMethodMetadata; private final MethodMetadata targetMethodMetadata;
private final CodeBlocks codeBlocks; private final CodeBlocks codeBlocks;
private final VariableNameFactory variableNameFactory;
AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod, AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod,
AotRepositoryFragmentMetadata targetTypeMetadata) { AotRepositoryFragmentMetadata targetTypeMetadata) {
@ -64,6 +63,7 @@ public class AotQueryMethodGenerationContext {
this.repositoryInformation = repositoryInformation; this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata; this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method); this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.codeBlocks = new CodeBlocks(targetTypeMetadata); this.codeBlocks = new CodeBlocks(targetTypeMetadata);
} }
@ -127,6 +127,16 @@ public class AotQueryMethodGenerationContext {
return TypeName.get(getReturnType().getType()); return TypeName.get(getReturnType().getType());
} }
/**
* Suggest naming clash free variant for the given intended variable name within the local method context.
*
* @param variableName the intended variable name.
* @return the suggested VariableName
*/
public String suggestLocalVariableName(String variableName) {
return variableNameFactory.generateName(variableName);
}
/** /**
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given * Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
* {@code parameterIndex} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its * {@code parameterIndex} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its
@ -227,7 +237,7 @@ public class AotQueryMethodGenerationContext {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
for (Parameter parameter : queryMethod.getParameters().getBindableParameters()) { for (Parameter parameter : queryMethod.getParameters().getBindableParameters()) {
parameter.getName().map(result::add); getParameterName(parameter.getIndex());
} }
return result; return result;
@ -237,14 +247,7 @@ public class AotQueryMethodGenerationContext {
* @return list of all parameter names (including non-bindable special parameters). * @return list of all parameter names (including non-bindable special parameters).
*/ */
public List<String> getAllParameterNames() { public List<String> getAllParameterNames() {
return targetMethodMetadata.getMethodArguments().keySet().stream().toList();
List<String> result = new ArrayList<>();
for (Parameter parameter : queryMethod.getParameters()) {
parameter.getName().map(result::add);
}
return result;
} }
public boolean hasField(String fieldName) { public boolean hasField(String fieldName) {
@ -269,17 +272,7 @@ public class AotQueryMethodGenerationContext {
} }
public @Nullable String getParameterName(int position) { public @Nullable String getParameterName(int position) {
return targetMethodMetadata.getParameterName(position);
if (0 > position) {
return null;
}
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(
targetMethodMetadata.getMethodArguments().entrySet());
if (position < entries.size()) {
return entries.get(position).getKey();
}
return null;
} }
public void addParameter(ParameterSpec parameter) { public void addParameter(ParameterSpec parameter) {

24
src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java

@ -16,7 +16,6 @@
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.TypeVariable; import java.lang.reflect.TypeVariable;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
@ -24,13 +23,8 @@ import java.util.stream.Collectors;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeVariableName; import org.springframework.javapoet.TypeVariableName;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -50,25 +44,7 @@ class AotRepositoryMethodBuilder {
private BiConsumer<AotQueryMethodGenerationContext, MethodSpec.Builder> customizer = (context, body) -> {}; private BiConsumer<AotQueryMethodGenerationContext, MethodSpec.Builder> customizer = (context, body) -> {};
AotRepositoryMethodBuilder(AotQueryMethodGenerationContext context) { AotRepositoryMethodBuilder(AotQueryMethodGenerationContext context) {
this.context = context; this.context = context;
initParameters(context.getMethod(), context.getRepositoryInformation());
}
private void initParameters(Method method, RepositoryInformation repositoryInformation) {
ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface());
for (Parameter parameter : method.getParameters()) {
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface);
TypeName parameterType = TypeName.get(resolvableParameterType.getType());
this.context.addParameter(ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build());
}
} }
/** /**

87
src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java

@ -0,0 +1,87 @@
/*
* Copyright 2025 the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.springframework.data.repository.aot.generate;
import java.util.Set;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Non thread safe {@link VariableNameFactory} implementation keeping track of defined names resolving name clashes
* using internal counters appending {@code _%d} to a suggested name in case of a clash.
*
* @author Christoph Strobl
* @since 4.0
*/
class LocalVariableNameFactory implements VariableNameFactory {
private final MultiValueMap<String, String> variables;
/**
* Create a new {@link LocalVariableNameFactory} considering available {@link MethodMetadata#getMethodArguments()
* method arguments}.
*
* @param methodMetadata source metadata
* @return new instance of {@link LocalVariableNameFactory}.
*/
static LocalVariableNameFactory forMethod(MethodMetadata methodMetadata) {
return of(methodMetadata.getMethodArguments().keySet());
}
/**
* Create a new {@link LocalVariableNameFactory} with a predefined set of initial variable names.
*
* @param predefinedVariables variables already known to be used in the given context.
* @return new instance of {@link LocalVariableNameFactory}.
*/
static LocalVariableNameFactory of(Set<String> predefinedVariables) {
return new LocalVariableNameFactory(predefinedVariables);
}
LocalVariableNameFactory(Iterable<String> predefinedVariableNames) {
variables = new LinkedMultiValueMap<>();
predefinedVariableNames.forEach(varName -> variables.add(varName, varName));
}
@Override
public String generateName(String intendedVariableName) {
if (!variables.containsKey(intendedVariableName)) {
variables.add(intendedVariableName, intendedVariableName);
return intendedVariableName;
}
String targetName = suggestTargetName(intendedVariableName);
variables.add(intendedVariableName, targetName);
variables.add(targetName, targetName);
return targetName;
}
String suggestTargetName(String suggested) {
return suggestTargetName(suggested, 1);
}
String suggestTargetName(String suggested, int counter) {
String targetName = "%s_%s".formatted(suggested, counter);
if (!variables.containsKey(targetName)) {
return targetName;
}
return suggestTargetName(suggested, counter + 1);
}
}

48
src/main/java/org/springframework/data/repository/aot/generate/MethodMetadata.java

@ -16,12 +16,16 @@
package org.springframework.data.repository.aot.generate; package org.springframework.data.repository.aot.generate;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.ParameterSpec;
@ -31,6 +35,7 @@ import org.springframework.javapoet.TypeName;
* Metadata about an AOT Repository method. * Metadata about an AOT Repository method.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.0
*/ */
class MethodMetadata { class MethodMetadata {
@ -38,10 +43,11 @@ class MethodMetadata {
private final ResolvableType actualReturnType; private final ResolvableType actualReturnType;
private final ResolvableType returnType; private final ResolvableType returnType;
public MethodMetadata(RepositoryInformation repositoryInformation, Method method) { MethodMetadata(RepositoryInformation repositoryInformation, Method method) {
this.returnType = repositoryInformation.getReturnType(method).toResolvableType(); this.returnType = repositoryInformation.getReturnType(method).toResolvableType();
this.actualReturnType = ResolvableType.forType(repositoryInformation.getReturnedDomainClass(method)); this.actualReturnType = ResolvableType.forType(repositoryInformation.getReturnedDomainClass(method));
this.initParameters(repositoryInformation, method, new DefaultParameterNameDiscoverer());
} }
@Nullable @Nullable
@ -54,20 +60,50 @@ class MethodMetadata {
return null; return null;
} }
public ResolvableType getReturnType() { ResolvableType getReturnType() {
return returnType; return returnType;
} }
public ResolvableType getActualReturnType() { ResolvableType getActualReturnType() {
return actualReturnType; return actualReturnType;
} }
public void addParameter(ParameterSpec parameterSpec) { void addParameter(ParameterSpec parameterSpec) {
this.methodArguments.put(parameterSpec.name, parameterSpec); this.methodArguments.put(parameterSpec.name, parameterSpec);
} }
public Map<String, ParameterSpec> getMethodArguments() { Map<String, ParameterSpec> getMethodArguments() {
return methodArguments; return methodArguments;
} }
@Nullable
String getParameterName(int position) {
if (0 > position) {
return null;
}
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(methodArguments.entrySet());
if (position < entries.size()) {
return entries.get(position).getKey();
}
return null;
}
private void initParameters(RepositoryInformation repositoryInformation, Method method,
ParameterNameDiscoverer nameDiscoverer) {
ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface());
for (java.lang.reflect.Parameter parameter : method.getParameters()) {
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
methodParameter.initParameterNameDiscovery(nameDiscoverer);
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface);
TypeName parameterType = TypeName.get(resolvableParameterType.getType());
addParameter(ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build());
}
}
} }

38
src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java

@ -0,0 +1,38 @@
/*
* Copyright 2025 the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.springframework.data.repository.aot.generate;
import org.springframework.lang.CheckReturnValue;
/**
* Name factory for generating clash free variable names checking an intended name against predefined and already used
* ones.
*
* @author Christoph Strobl
* @since 4.0
*/
interface VariableNameFactory {
/**
* Compare and potentially generate a new name for the given intended variable name.
*
* @param intendedVariableName must not be {@literal null}.
* @return the {@literal intendedVariableName} if no naming clash detected or a clash free generated name.
*/
@CheckReturnValue
String generateName(String intendedVariableName);
}

70
src/test/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContextUnitTests.java

@ -0,0 +1,70 @@
/*
* Copyright 2025 the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.TypeInformation;
/**
* Tests targeting {@link AotQueryMethodGenerationContext}.
*
* @author Christoph Strobl
*/
class AotQueryMethodGenerationContextUnitTests {
@Test // GH-3270
void suggestLocalVariableNameConsidersMethodArguments() throws NoSuchMethodException {
AotQueryMethodGenerationContext ctx = ctxFor("reservedParameterMethod");
assertThat(ctx.suggestLocalVariableName("foo")).isEqualTo("foo");
assertThat(ctx.suggestLocalVariableName("arg0")).isNotIn("arg0", "arg1", "arg2");
}
AotQueryMethodGenerationContext ctxFor(String methodName) throws NoSuchMethodException {
Method target = null;
for (Method m : DummyRepo.class.getMethods()) {
if (m.getName().equals(methodName)) {
target = m;
break;
}
}
if (target == null) {
throw new NoSuchMethodException(methodName);
}
RepositoryInformation ri = Mockito.mock(RepositoryInformation.class);
Mockito.doReturn(TypeInformation.of(target.getReturnType())).when(ri).getReturnType(eq(target));
return new AotQueryMethodGenerationContext(ri, target, Mockito.mock(QueryMethod.class),
Mockito.mock(AotRepositoryFragmentMetadata.class));
}
private interface DummyRepo {
String reservedParameterMethod(Object arg0, Pageable arg1, Object arg2);
}
}

72
src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java

@ -0,0 +1,72 @@
/*
* Copyright 2025 the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* @author Christoph Strobl
*/
class LocalVariableNameFactoryUnitTests {
LocalVariableNameFactory variableNameFactory;
@BeforeEach
void beforeEach() {
variableNameFactory = LocalVariableNameFactory.of(Set.of("firstname", "lastname", "sort"));
}
@Test // GH-3270
void resolvesNameClashesInNames() {
assertThat(variableNameFactory.generateName("name")).isEqualTo("name");
assertThat(variableNameFactory.generateName("name")).isEqualTo("name_1");
assertThat(variableNameFactory.generateName("name")).isEqualTo("name_2");
assertThat(variableNameFactory.generateName("name1")).isEqualTo("name1");
assertThat(variableNameFactory.generateName("name3")).isEqualTo("name3");
assertThat(variableNameFactory.generateName("name3")).isEqualTo("name3_1");
assertThat(variableNameFactory.generateName("name4_1")).isEqualTo("name4_1");
assertThat(variableNameFactory.generateName("name4")).isEqualTo("name4");
assertThat(variableNameFactory.generateName("name4_1_1")).isEqualTo("name4_1_1");
assertThat(variableNameFactory.generateName("name4_1")).isEqualTo("name4_1_2");
assertThat(variableNameFactory.generateName("name4_1")).isEqualTo("name4_1_3");
}
@Test // GH-3270
void worksWithVariablesContainingUnderscores() {
assertThat(variableNameFactory.generateName("first_name")).isEqualTo("first_name");
assertThat(variableNameFactory.generateName("first_name")).isEqualTo("first_name_1");
assertThat(variableNameFactory.generateName("first_name")).isEqualTo("first_name_2");
assertThat(variableNameFactory.generateName("first_name_3")).isEqualTo("first_name_3");
assertThat(variableNameFactory.generateName("first_name")).isEqualTo("first_name_4");
}
@Test // GH-3270
void considersPredefinedNames() {
assertThat(variableNameFactory.generateName("firstname")).isEqualTo("firstname_1");
}
@Test // GH-3270
void considersCase() {
assertThat(variableNameFactory.generateName("firstName")).isEqualTo("firstName");
}
}

85
src/test/java/org/springframework/data/repository/aot/generate/MethodMetadataUnitTests.java

@ -0,0 +1,85 @@
/*
* Copyright 2025 the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.springframework.data.repository.aot.generate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.util.TypeInformation;
/**
* Unit tests for {@link MethodMetadata}.
*
* @author Christoph Strobl
*/
class MethodMetadataUnitTests {
@Test // GH-3270
void getParameterNameByIndex() throws NoSuchMethodException {
MethodMetadata metadata = methodMetadataFor("threeArgsMethod");
assertThat(metadata.getParameterName(0)).isEqualTo("arg0");
assertThat(metadata.getParameterName(1)).isEqualTo("arg1");
assertThat(metadata.getParameterName(2)).isEqualTo("arg2");
}
@Test // GH-3270
void getParameterNameByNonExistingIndex() throws NoSuchMethodException {
MethodMetadata metadata = methodMetadataFor("threeArgsMethod");
assertThat(metadata.getParameterName(-1)).isNull();
assertThat(metadata.getParameterName(3)).isNull();
}
@Test // GH-3270
void getParameterNameForNoArgsMethod() throws NoSuchMethodException {
assertThat(methodMetadataFor("noArgsMethod").getParameterName(0)).isNull();
}
static MethodMetadata methodMetadataFor(String methodName) throws NoSuchMethodException {
Method target = null;
for (Method m : DummyRepo.class.getMethods()) {
if (m.getName().equals(methodName)) {
target = m;
break;
}
}
if (target == null) {
throw new NoSuchMethodException(methodName);
}
RepositoryInformation ri = Mockito.mock(RepositoryInformation.class);
Mockito.doReturn(TypeInformation.of(target.getReturnType())).when(ri).getReturnType(eq(target));
return new MethodMetadata(ri, target);
}
private interface DummyRepo {
String noArgsMethod();
String threeArgsMethod(Object arg0, Pageable arg1, Object arg2);
}
}
Loading…
Cancel
Save