diff --git a/pom.xml b/pom.xml index a6dc167a03..1ba673449d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.x-GH-3270-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java index 0dd0806637..fc4fb6a6eb 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java @@ -24,11 +24,11 @@ import javax.lang.model.element.Modifier; import org.jspecify.annotations.Nullable; - import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotationSelectors; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.repository.aot.generate.VariableNameFactory.VariableName; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.QueryMethod; @@ -54,6 +54,7 @@ public class AotQueryMethodGenerationContext { private final AotRepositoryFragmentMetadata targetTypeMetadata; private final MethodMetadata targetMethodMetadata; private final CodeBlocks codeBlocks; + private final VariableNameFactory variableNameFactory; AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod, AotRepositoryFragmentMetadata targetTypeMetadata) { @@ -65,6 +66,7 @@ public class AotQueryMethodGenerationContext { this.targetTypeMetadata = targetTypeMetadata; this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method); this.codeBlocks = new CodeBlocks(targetTypeMetadata); + this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata); } AotRepositoryFragmentMetadata getTargetTypeMetadata() { @@ -127,6 +129,16 @@ public TypeName getReturnTypeName() { return TypeName.get(getReturnType().getType()); } + /** + * Suggests 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 VariableName suggestLocalVariableName(String variableName) { + return variableNameFactory.generateName(variableName); + } + /** * 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 @@ -274,8 +286,7 @@ public String getParameterNameOf(Class type) { return null; } - List> entries = new ArrayList<>( - targetMethodMetadata.getMethodArguments().entrySet()); + List> entries = new ArrayList<>(targetMethodMetadata.getMethodArguments().entrySet()); if (position < entries.size()) { return entries.get(position).getKey(); } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java b/src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java new file mode 100644 index 0000000000..63b56d1c28 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java @@ -0,0 +1,82 @@ +/* + * 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; + +/** + * {@link VariableNameFactory} implementation keeping track of defined names resolving name clashes using internal + * counter 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 variables; + + static LocalVariableNameFactory forMethod(MethodMetadata methodMetadata) { + return of(methodMetadata.getMethodArguments().keySet()); + } + + static LocalVariableNameFactory empty() { + return of(Set.of()); + } + + static LocalVariableNameFactory of(Set variables) { + return new LocalVariableNameFactory(variables); + } + + LocalVariableNameFactory(Iterable predefinedVariableNames) { + + variables = new LinkedMultiValueMap<>(); + for (String parameterName : predefinedVariableNames) { + variables.add(parameterName, new VariableName(parameterName)); + } + } + + @Override + public VariableName generateName(String suggestedName) { + + if (!variables.containsKey(suggestedName)) { + VariableName variableName = new VariableName(suggestedName); + variables.add(suggestedName, variableName); + return variableName; + } + + String targetName = suggestTargetName(suggestedName); + VariableName variableName = new VariableName(suggestedName, targetName); + variables.add(suggestedName, variableName); + variables.add(targetName, variableName); + return variableName; + } + + 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); + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java b/src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java new file mode 100644 index 0000000000..1111474b04 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * @author Christoph Strobl + * @since 4.0 + */ +public interface VariableNameFactory { + + VariableName generateName(String suggestedName); + + record VariableName(String source, String target) { + + public VariableName(String source) { + this(source, source); + } + + public String name() { + return target; + } + } +} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java new file mode 100644 index 0000000000..63cfc110a1 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java @@ -0,0 +1,62 @@ +/* + * 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").name()).isEqualTo("name"); + assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name_1"); + assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name_2"); + assertThat(variableNameFactory.generateName("name1").name()).isEqualTo("name1"); + assertThat(variableNameFactory.generateName("name3").name()).isEqualTo("name3"); + assertThat(variableNameFactory.generateName("name3").name()).isEqualTo("name3_1"); + assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1"); + assertThat(variableNameFactory.generateName("name4").name()).isEqualTo("name4"); + assertThat(variableNameFactory.generateName("name4_1_1").name()).isEqualTo("name4_1_1"); + assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1_2"); + assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1_3"); + } + + @Test // GH-3270 + void considersPredefinedNames() { + assertThat(variableNameFactory.generateName("firstname").name()).isEqualTo("firstname_1"); + } + + @Test // GH-3270 + void considersCase() { + assertThat(variableNameFactory.generateName("firstName").name()).isEqualTo("firstName"); + } +}