Browse Source
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: #3271pull/3304/head
8 changed files with 409 additions and 52 deletions
@ -0,0 +1,87 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -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); |
||||
|
||||
} |
||||
@ -0,0 +1,70 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,72 @@
@@ -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"); |
||||
} |
||||
} |
||||
@ -0,0 +1,85 @@
@@ -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…
Reference in new issue