Skip to content

Commit 10f4ee9

Browse files
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.
1 parent 290b059 commit 10f4ee9

File tree

4 files changed

+194
-3
lines changed

4 files changed

+194
-3
lines changed

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
import javax.lang.model.element.Modifier;
2525

2626
import org.jspecify.annotations.Nullable;
27-
2827
import org.springframework.core.ResolvableType;
2928
import org.springframework.core.annotation.MergedAnnotation;
3029
import org.springframework.core.annotation.MergedAnnotationSelectors;
3130
import org.springframework.core.annotation.MergedAnnotations;
31+
import org.springframework.data.repository.aot.generate.VariableNameFactory.VariableName;
3232
import org.springframework.data.repository.core.RepositoryInformation;
3333
import org.springframework.data.repository.query.Parameter;
3434
import org.springframework.data.repository.query.QueryMethod;
@@ -54,6 +54,7 @@ public class AotQueryMethodGenerationContext {
5454
private final AotRepositoryFragmentMetadata targetTypeMetadata;
5555
private final MethodMetadata targetMethodMetadata;
5656
private final CodeBlocks codeBlocks;
57+
private final VariableNameFactory variableNameFactory;
5758

5859
AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod,
5960
AotRepositoryFragmentMetadata targetTypeMetadata) {
@@ -65,6 +66,7 @@ public class AotQueryMethodGenerationContext {
6566
this.targetTypeMetadata = targetTypeMetadata;
6667
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
6768
this.codeBlocks = new CodeBlocks(targetTypeMetadata);
69+
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
6870
}
6971

7072
AotRepositoryFragmentMetadata getTargetTypeMetadata() {
@@ -127,6 +129,16 @@ public TypeName getReturnTypeName() {
127129
return TypeName.get(getReturnType().getType());
128130
}
129131

132+
/**
133+
* Suggests naming clash free variant for the given intended variable name within the local method context.
134+
*
135+
* @param variableName the intended variable name.
136+
* @return the suggested VariableName
137+
*/
138+
public VariableName suggestLocalVariableName(String variableName) {
139+
return variableNameFactory.generateName(variableName);
140+
}
141+
130142
/**
131143
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
132144
* {@code parameterIndex} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its
@@ -274,8 +286,7 @@ public String getParameterNameOf(Class<?> type) {
274286
return null;
275287
}
276288

277-
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(
278-
targetMethodMetadata.getMethodArguments().entrySet());
289+
List<Entry<String, ParameterSpec>> entries = new ArrayList<>(targetMethodMetadata.getMethodArguments().entrySet());
279290
if (position < entries.size()) {
280291
return entries.get(position).getKey();
281292
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
* {@link VariableNameFactory} implementation keeping track of defined names resolving name clashes using internal
25+
* counter 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, VariableName> variables;
33+
34+
static LocalVariableNameFactory forMethod(MethodMetadata methodMetadata) {
35+
return of(methodMetadata.getMethodArguments().keySet());
36+
}
37+
38+
static LocalVariableNameFactory empty() {
39+
return of(Set.of());
40+
}
41+
42+
static LocalVariableNameFactory of(Set<String> variables) {
43+
return new LocalVariableNameFactory(variables);
44+
}
45+
46+
LocalVariableNameFactory(Iterable<String> predefinedVariableNames) {
47+
48+
variables = new LinkedMultiValueMap<>();
49+
for (String parameterName : predefinedVariableNames) {
50+
variables.add(parameterName, new VariableName(parameterName));
51+
}
52+
}
53+
54+
@Override
55+
public VariableName generateName(String suggestedName) {
56+
57+
if (!variables.containsKey(suggestedName)) {
58+
VariableName variableName = new VariableName(suggestedName);
59+
variables.add(suggestedName, variableName);
60+
return variableName;
61+
}
62+
63+
String targetName = suggestTargetName(suggestedName);
64+
VariableName variableName = new VariableName(suggestedName, targetName);
65+
variables.add(suggestedName, variableName);
66+
variables.add(targetName, variableName);
67+
return variableName;
68+
}
69+
70+
String suggestTargetName(String suggested) {
71+
return suggestTargetName(suggested, 1);
72+
}
73+
74+
String suggestTargetName(String suggested, int counter) {
75+
76+
String targetName = "%s_%s".formatted(suggested, counter);
77+
if (!variables.containsKey(targetName)) {
78+
return targetName;
79+
}
80+
return suggestTargetName(suggested, counter + 1);
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
/**
19+
* @author Christoph Strobl
20+
* @since 4.0
21+
*/
22+
public interface VariableNameFactory {
23+
24+
VariableName generateName(String suggestedName);
25+
26+
record VariableName(String source, String target) {
27+
28+
public VariableName(String source) {
29+
this(source, source);
30+
}
31+
32+
public String name() {
33+
return target;
34+
}
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Set;
21+
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
25+
/**
26+
* @author Christoph Strobl
27+
*/
28+
class LocalVariableNameFactoryUnitTests {
29+
30+
LocalVariableNameFactory variableNameFactory;
31+
32+
@BeforeEach
33+
void beforeEach() {
34+
variableNameFactory = LocalVariableNameFactory.of(Set.of("firstname", "lastname", "sort"));
35+
}
36+
37+
@Test // GH-3270
38+
void resolvesNameClashesInNames() {
39+
40+
assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name");
41+
assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name_1");
42+
assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name_2");
43+
assertThat(variableNameFactory.generateName("name1").name()).isEqualTo("name1");
44+
assertThat(variableNameFactory.generateName("name3").name()).isEqualTo("name3");
45+
assertThat(variableNameFactory.generateName("name3").name()).isEqualTo("name3_1");
46+
assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1");
47+
assertThat(variableNameFactory.generateName("name4").name()).isEqualTo("name4");
48+
assertThat(variableNameFactory.generateName("name4_1_1").name()).isEqualTo("name4_1_1");
49+
assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1_2");
50+
assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1_3");
51+
}
52+
53+
@Test // GH-3270
54+
void considersPredefinedNames() {
55+
assertThat(variableNameFactory.generateName("firstname").name()).isEqualTo("firstname_1");
56+
}
57+
58+
@Test // GH-3270
59+
void considersCase() {
60+
assertThat(variableNameFactory.generateName("firstName").name()).isEqualTo("firstName");
61+
}
62+
}

0 commit comments

Comments
 (0)