Skip to content

Commit baf3cb8

Browse files
christophstroblmp911de
authored andcommitted
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
1 parent 9aff766 commit baf3cb8

File tree

8 files changed

+409
-52
lines changed

8 files changed

+409
-52
lines changed

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

+15-22
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@
1919
import java.lang.reflect.Method;
2020
import java.util.ArrayList;
2121
import java.util.List;
22-
import java.util.Map.Entry;
2322

2423
import javax.lang.model.element.Modifier;
2524

2625
import org.jspecify.annotations.Nullable;
27-
2826
import org.springframework.core.ResolvableType;
2927
import org.springframework.core.annotation.MergedAnnotation;
3028
import org.springframework.core.annotation.MergedAnnotationSelectors;
@@ -54,6 +52,7 @@ public class AotQueryMethodGenerationContext {
5452
private final AotRepositoryFragmentMetadata targetTypeMetadata;
5553
private final MethodMetadata targetMethodMetadata;
5654
private final CodeBlocks codeBlocks;
55+
private final VariableNameFactory variableNameFactory;
5756

5857
AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod,
5958
AotRepositoryFragmentMetadata targetTypeMetadata) {
@@ -64,6 +63,7 @@ public class AotQueryMethodGenerationContext {
6463
this.repositoryInformation = repositoryInformation;
6564
this.targetTypeMetadata = targetTypeMetadata;
6665
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
66+
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
6767
this.codeBlocks = new CodeBlocks(targetTypeMetadata);
6868
}
6969

@@ -127,6 +127,16 @@ public TypeName getReturnTypeName() {
127127
return TypeName.get(getReturnType().getType());
128128
}
129129

130+
/**
131+
* Suggest naming clash free variant for the given intended variable name within the local method context.
132+
*
133+
* @param variableName the intended variable name.
134+
* @return the suggested VariableName
135+
*/
136+
public String suggestLocalVariableName(String variableName) {
137+
return variableNameFactory.generateName(variableName);
138+
}
139+
130140
/**
131141
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
132142
* {@code parameterIndex} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its
@@ -227,7 +237,7 @@ public List<String> getBindableParameterNames() {
227237
List<String> result = new ArrayList<>();
228238

229239
for (Parameter parameter : queryMethod.getParameters().getBindableParameters()) {
230-
parameter.getName().map(result::add);
240+
getParameterName(parameter.getIndex());
231241
}
232242

233243
return result;
@@ -237,14 +247,7 @@ public List<String> getBindableParameterNames() {
237247
* @return list of all parameter names (including non-bindable special parameters).
238248
*/
239249
public List<String> getAllParameterNames() {
240-
241-
List<String> result = new ArrayList<>();
242-
243-
for (Parameter parameter : queryMethod.getParameters()) {
244-
parameter.getName().map(result::add);
245-
}
246-
247-
return result;
250+
return targetMethodMetadata.getMethodArguments().keySet().stream().toList();
248251
}
249252

250253
public boolean hasField(String fieldName) {
@@ -269,17 +272,7 @@ public String getParameterNameOf(Class<?> type) {
269272
}
270273

271274
public @Nullable String getParameterName(int position) {
272-
273-
if (0 > position) {
274-
return null;
275-
}
276-
277-
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(
278-
targetMethodMetadata.getMethodArguments().entrySet());
279-
if (position < entries.size()) {
280-
return entries.get(position).getKey();
281-
}
282-
return null;
275+
return targetMethodMetadata.getParameterName(position);
283276
}
284277

285278
public void addParameter(ParameterSpec parameter) {

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

-24
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,15 @@
1616
package org.springframework.data.repository.aot.generate;
1717

1818
import java.lang.reflect.Method;
19-
import java.lang.reflect.Parameter;
2019
import java.lang.reflect.TypeVariable;
2120
import java.util.function.BiConsumer;
2221
import java.util.function.Function;
2322
import java.util.stream.Collectors;
2423

2524
import javax.lang.model.element.Modifier;
2625

27-
import org.springframework.core.DefaultParameterNameDiscoverer;
28-
import org.springframework.core.MethodParameter;
29-
import org.springframework.core.ResolvableType;
30-
import org.springframework.data.repository.core.RepositoryInformation;
3126
import org.springframework.javapoet.CodeBlock;
3227
import org.springframework.javapoet.MethodSpec;
33-
import org.springframework.javapoet.ParameterSpec;
3428
import org.springframework.javapoet.TypeName;
3529
import org.springframework.javapoet.TypeVariableName;
3630
import org.springframework.util.StringUtils;
@@ -50,25 +44,7 @@ class AotRepositoryMethodBuilder {
5044
private BiConsumer<AotQueryMethodGenerationContext, MethodSpec.Builder> customizer = (context, body) -> {};
5145

5246
AotRepositoryMethodBuilder(AotQueryMethodGenerationContext context) {
53-
5447
this.context = context;
55-
initParameters(context.getMethod(), context.getRepositoryInformation());
56-
}
57-
58-
private void initParameters(Method method, RepositoryInformation repositoryInformation) {
59-
60-
ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface());
61-
62-
for (Parameter parameter : method.getParameters()) {
63-
64-
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
65-
methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
66-
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface);
67-
68-
TypeName parameterType = TypeName.get(resolvableParameterType.getType());
69-
70-
this.context.addParameter(ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build());
71-
}
7248
}
7349

7450
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.aot.generate;
17+
18+
import java.util.Set;
19+
20+
import org.springframework.util.LinkedMultiValueMap;
21+
import org.springframework.util.MultiValueMap;
22+
23+
/**
24+
* Non thread safe {@link VariableNameFactory} implementation keeping track of defined names resolving name clashes
25+
* using internal counters appending {@code _%d} to a suggested name in case of a clash.
26+
*
27+
* @author Christoph Strobl
28+
* @since 4.0
29+
*/
30+
class LocalVariableNameFactory implements VariableNameFactory {
31+
32+
private final MultiValueMap<String, String> variables;
33+
34+
/**
35+
* Create a new {@link LocalVariableNameFactory} considering available {@link MethodMetadata#getMethodArguments()
36+
* method arguments}.
37+
*
38+
* @param methodMetadata source metadata
39+
* @return new instance of {@link LocalVariableNameFactory}.
40+
*/
41+
static LocalVariableNameFactory forMethod(MethodMetadata methodMetadata) {
42+
return of(methodMetadata.getMethodArguments().keySet());
43+
}
44+
45+
/**
46+
* Create a new {@link LocalVariableNameFactory} with a predefined set of initial variable names.
47+
*
48+
* @param predefinedVariables variables already known to be used in the given context.
49+
* @return new instance of {@link LocalVariableNameFactory}.
50+
*/
51+
static LocalVariableNameFactory of(Set<String> predefinedVariables) {
52+
return new LocalVariableNameFactory(predefinedVariables);
53+
}
54+
55+
LocalVariableNameFactory(Iterable<String> predefinedVariableNames) {
56+
57+
variables = new LinkedMultiValueMap<>();
58+
predefinedVariableNames.forEach(varName -> variables.add(varName, varName));
59+
}
60+
61+
@Override
62+
public String generateName(String intendedVariableName) {
63+
64+
if (!variables.containsKey(intendedVariableName)) {
65+
variables.add(intendedVariableName, intendedVariableName);
66+
return intendedVariableName;
67+
}
68+
69+
String targetName = suggestTargetName(intendedVariableName);
70+
variables.add(intendedVariableName, targetName);
71+
variables.add(targetName, targetName);
72+
return targetName;
73+
}
74+
75+
String suggestTargetName(String suggested) {
76+
return suggestTargetName(suggested, 1);
77+
}
78+
79+
String suggestTargetName(String suggested, int counter) {
80+
81+
String targetName = "%s_%s".formatted(suggested, counter);
82+
if (!variables.containsKey(targetName)) {
83+
return targetName;
84+
}
85+
return suggestTargetName(suggested, counter + 1);
86+
}
87+
}

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

+42-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
package org.springframework.data.repository.aot.generate;
1717

1818
import java.lang.reflect.Method;
19+
import java.util.ArrayList;
1920
import java.util.LinkedHashMap;
21+
import java.util.List;
2022
import java.util.Map;
2123
import java.util.Map.Entry;
2224

2325
import org.jspecify.annotations.Nullable;
24-
26+
import org.springframework.core.DefaultParameterNameDiscoverer;
27+
import org.springframework.core.MethodParameter;
28+
import org.springframework.core.ParameterNameDiscoverer;
2529
import org.springframework.core.ResolvableType;
2630
import org.springframework.data.repository.core.RepositoryInformation;
2731
import org.springframework.javapoet.ParameterSpec;
@@ -31,17 +35,19 @@
3135
* Metadata about an AOT Repository method.
3236
*
3337
* @author Christoph Strobl
38+
* @since 4.0
3439
*/
3540
class MethodMetadata {
3641

3742
private final Map<String, ParameterSpec> methodArguments = new LinkedHashMap<>();
3843
private final ResolvableType actualReturnType;
3944
private final ResolvableType returnType;
4045

41-
public MethodMetadata(RepositoryInformation repositoryInformation, Method method) {
46+
MethodMetadata(RepositoryInformation repositoryInformation, Method method) {
4247

4348
this.returnType = repositoryInformation.getReturnType(method).toResolvableType();
4449
this.actualReturnType = ResolvableType.forType(repositoryInformation.getReturnedDomainClass(method));
50+
this.initParameters(repositoryInformation, method, new DefaultParameterNameDiscoverer());
4551
}
4652

4753
@Nullable
@@ -54,20 +60,50 @@ public String getParameterNameOf(Class<?> type) {
5460
return null;
5561
}
5662

57-
public ResolvableType getReturnType() {
63+
ResolvableType getReturnType() {
5864
return returnType;
5965
}
6066

61-
public ResolvableType getActualReturnType() {
67+
ResolvableType getActualReturnType() {
6268
return actualReturnType;
6369
}
6470

65-
public void addParameter(ParameterSpec parameterSpec) {
71+
void addParameter(ParameterSpec parameterSpec) {
6672
this.methodArguments.put(parameterSpec.name, parameterSpec);
6773
}
6874

69-
public Map<String, ParameterSpec> getMethodArguments() {
75+
Map<String, ParameterSpec> getMethodArguments() {
7076
return methodArguments;
7177
}
7278

79+
@Nullable
80+
String getParameterName(int position) {
81+
82+
if (0 > position) {
83+
return null;
84+
}
85+
86+
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(methodArguments.entrySet());
87+
if (position < entries.size()) {
88+
return entries.get(position).getKey();
89+
}
90+
return null;
91+
}
92+
93+
private void initParameters(RepositoryInformation repositoryInformation, Method method,
94+
ParameterNameDiscoverer nameDiscoverer) {
95+
96+
ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface());
97+
98+
for (java.lang.reflect.Parameter parameter : method.getParameters()) {
99+
100+
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
101+
methodParameter.initParameterNameDiscovery(nameDiscoverer);
102+
ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(methodParameter, repositoryInterface);
103+
104+
TypeName parameterType = TypeName.get(resolvableParameterType.getType());
105+
106+
addParameter(ParameterSpec.builder(parameterType, methodParameter.getParameterName()).build());
107+
}
108+
}
73109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.aot.generate;
17+
18+
import org.springframework.lang.CheckReturnValue;
19+
20+
/**
21+
* Name factory for generating clash free variable names checking an intended name against predefined and already used
22+
* ones.
23+
*
24+
* @author Christoph Strobl
25+
* @since 4.0
26+
*/
27+
interface VariableNameFactory {
28+
29+
/**
30+
* Compare and potentially generate a new name for the given intended variable name.
31+
*
32+
* @param intendedVariableName must not be {@literal null}.
33+
* @return the {@literal intendedVariableName} if no naming clash detected or a clash free generated name.
34+
*/
35+
@CheckReturnValue
36+
String generateName(String intendedVariableName);
37+
38+
}

0 commit comments

Comments
 (0)