From f8d9847b0a0c43505cc5f5a5dd09956b65c90835 Mon Sep 17 00:00:00 2001 From: Andy Boothe Date: Sun, 12 Jan 2025 20:44:22 -0600 Subject: [PATCH] add unit tests for ConversionExprFactory implementations --- pom.xml | 21 +- rapier-core/pom.xml | 5 + .../ElementwiseListConversionExprFactory.java | 23 +- .../expr/IdentityConversionExprFactory.java | 10 - ...tringToCharacterConversionExprFactory.java | 14 +- ...tringToPrimitiveConversionExprFactory.java | 7 +- .../test/java/rapier/core/RapierTestBase.java | 2 +- ...mentwiseListConversionExprFactoryTest.java | 132 +++++++++ .../FromStringConversionExprFactoryTest.java | 246 +++++++++++++++++ .../IdentityConversionExprFactoryTest.java | 67 +++++ ...gleArgumentConstructorExprFactoryTest.java | 217 +++++++++++++++ .../StringToCharacterExprFactoryTest.java | 61 +++++ .../StringToPrimitiveExprFactoryTest.java | 167 ++++++++++++ .../ValueOfConversionExprFactoryTest.java | 254 ++++++++++++++++++ 14 files changed, 1187 insertions(+), 39 deletions(-) create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactoryTest.java create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/FromStringConversionExprFactoryTest.java create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/IdentityConversionExprFactoryTest.java create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/SingleArgumentConstructorExprFactoryTest.java create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/StringToCharacterExprFactoryTest.java create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/StringToPrimitiveExprFactoryTest.java create mode 100644 rapier-core/src/test/java/rapier/core/conversion/expr/ValueOfConversionExprFactoryTest.java diff --git a/pom.xml b/pom.xml index e4a7704..5580da3 100644 --- a/pom.xml +++ b/pom.xml @@ -69,10 +69,18 @@ maven-surefire-plugin 3.5.2 - - + + + -Xshare:off --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-opens @@ -226,6 +234,13 @@ pom import + + org.mockito + mockito-bom + 5.15.2 + pom + import + com.google.dagger dagger diff --git a/rapier-core/pom.xml b/rapier-core/pom.xml index 0b214b7..d26bf65 100644 --- a/rapier-core/pom.xml +++ b/rapier-core/pom.xml @@ -61,5 +61,10 @@ junit-jupiter test + + org.mockito + mockito-core + test + diff --git a/rapier-core/src/main/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactory.java b/rapier-core/src/main/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactory.java index 2a0ce10..4ed6cea 100644 --- a/rapier-core/src/main/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactory.java +++ b/rapier-core/src/main/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactory.java @@ -22,7 +22,6 @@ import static java.util.Objects.requireNonNull; import java.util.List; import java.util.Optional; -import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -40,29 +39,29 @@ public ElementwiseListConversionExprFactory(Types types, } @Override - @SuppressWarnings("unused") public Optional generateConversionExpr(TypeMirror targetType, String sourceValue) { if (targetType.getKind() != TypeKind.DECLARED) return Optional.empty(); - final TypeElement targetElement = (TypeElement) getTypes().asElement(targetType); final DeclaredType targetDeclaredType = (DeclaredType) targetType; - // Get type arguments + final TypeMirror targetErasedType = getTypes().erasure(targetType); + if (!targetErasedType.toString().equals("java.util.List")) + return Optional.empty(); + + // Get the type arguments of the target type. If there are no type arguments, then this is a + // raw List type, and we can't see the element type, so just return. List typeArguments = targetDeclaredType.getTypeArguments(); - - // Ensure there's exactly one type argument. There might be zero, but there really, really - // shouldn't be more than 1. - if (typeArguments.size() != 1) + if (typeArguments.size() == 0) return Optional.empty(); - // Get the first type argument - final TypeMirror targetTypeArgument = typeArguments.get(0); - if (targetTypeArgument.getKind() != TypeKind.DECLARED) + // Get the type argument. If it's not an exact declared type, then we're done. + final TypeMirror targeElementType = typeArguments.get(0); + if (targeElementType.getKind() != TypeKind.DECLARED) return Optional.empty(); // Generate conversion expression for each element final Optional maybeElementConversionExpr = - getElementConversionExprFactory().generateConversionExpr(targetTypeArgument, "element"); + getElementConversionExprFactory().generateConversionExpr(targeElementType, "element"); if (maybeElementConversionExpr.isEmpty()) return Optional.empty(); final String elementConversionExpr = maybeElementConversionExpr.orElseThrow(); diff --git a/rapier-core/src/main/java/rapier/core/conversion/expr/IdentityConversionExprFactory.java b/rapier-core/src/main/java/rapier/core/conversion/expr/IdentityConversionExprFactory.java index ef54332..7d59382 100644 --- a/rapier-core/src/main/java/rapier/core/conversion/expr/IdentityConversionExprFactory.java +++ b/rapier-core/src/main/java/rapier/core/conversion/expr/IdentityConversionExprFactory.java @@ -21,9 +21,6 @@ import static java.util.Objects.requireNonNull; import java.util.Optional; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import rapier.core.ConversionExprFactory; @@ -38,16 +35,9 @@ public IdentityConversionExprFactory(Types types, TypeMirror sourceType) { } @Override - @SuppressWarnings("unused") public Optional generateConversionExpr(TypeMirror targetType, String sourceValue) { - if (targetType.getKind() != TypeKind.DECLARED) - return Optional.empty(); - final TypeElement targetElement = (TypeElement) getTypes().asElement(targetType); - final DeclaredType targetDeclaredType = (DeclaredType) targetType; - if (getTypes().isSameType(targetType, getSourceType())) return Optional.of(sourceValue); - return Optional.empty(); } diff --git a/rapier-core/src/main/java/rapier/core/conversion/expr/StringToCharacterConversionExprFactory.java b/rapier-core/src/main/java/rapier/core/conversion/expr/StringToCharacterConversionExprFactory.java index 46b7ea3..220275d 100644 --- a/rapier-core/src/main/java/rapier/core/conversion/expr/StringToCharacterConversionExprFactory.java +++ b/rapier-core/src/main/java/rapier/core/conversion/expr/StringToCharacterConversionExprFactory.java @@ -21,9 +21,6 @@ import static java.util.Objects.requireNonNull; import java.util.Optional; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import rapier.core.ConversionExprFactory; @@ -36,20 +33,15 @@ public StringToCharacterConversionExprFactory(Types types) { } @Override - @SuppressWarnings("unused") public Optional generateConversionExpr(TypeMirror targetType, String sourceValue) { - if (targetType.getKind() != TypeKind.DECLARED) - return Optional.empty(); - final TypeElement targetElement = (TypeElement) getTypes().asElement(targetType); - final DeclaredType targetDeclaredType = (DeclaredType) targetType; - - if (!targetDeclaredType.toString().equals("java.lang.Character")) + if (!targetType.toString().equals("java.lang.Character")) return Optional.empty(); return Optional.of("Optional.of(" + sourceValue - + ").filter(s -> !s.isEmpty()).map(s -> Character.valueOf(s.charAt(0))).orElseThrow(() -> new IllegalStateException(\"Cannot convert empty string to char\"))"); + + ").map(s -> s.isEmpty() ? null : s.charAt(0)).orElseThrow(() -> new IllegalStateException(\"Cannot convert empty string to char\"))"); } + @SuppressWarnings("unused") private Types getTypes() { return types; } diff --git a/rapier-core/src/main/java/rapier/core/conversion/expr/StringToPrimitiveConversionExprFactory.java b/rapier-core/src/main/java/rapier/core/conversion/expr/StringToPrimitiveConversionExprFactory.java index 8a4d2c3..95c9e9f 100644 --- a/rapier-core/src/main/java/rapier/core/conversion/expr/StringToPrimitiveConversionExprFactory.java +++ b/rapier-core/src/main/java/rapier/core/conversion/expr/StringToPrimitiveConversionExprFactory.java @@ -34,6 +34,8 @@ public StringToPrimitiveConversionExprFactory(Types types) { @Override public Optional generateConversionExpr(TypeMirror targetType, String sourceValue) { + if (!targetType.getKind().isPrimitive()) + return Optional.empty(); switch (targetType.getKind()) { case BOOLEAN: return Optional.of("Boolean.parseBoolean(" + sourceValue + ")"); @@ -46,13 +48,14 @@ public Optional generateConversionExpr(TypeMirror targetType, String sou case LONG: return Optional.of("Long.parseLong(" + sourceValue + ")"); case CHAR: - return Optional.of(sourceValue + ".charAt(0)"); + return Optional.of("Optional.of(" + sourceValue + + ").map(s -> s.isEmpty() ? null : s.charAt(0)).orElseThrow(() -> new IllegalStateException(\"Cannot convert empty string to char\"))"); case FLOAT: return Optional.of("Float.parseFloat(" + sourceValue + ")"); case DOUBLE: return Optional.of("Double.parseDouble(" + sourceValue + ")"); default: - return Optional.empty(); + throw new AssertionError("Unexpected primitive type: " + targetType); } } diff --git a/rapier-core/src/test/java/rapier/core/RapierTestBase.java b/rapier-core/src/test/java/rapier/core/RapierTestBase.java index 19741cd..31bf79e 100644 --- a/rapier-core/src/test/java/rapier/core/RapierTestBase.java +++ b/rapier-core/src/test/java/rapier/core/RapierTestBase.java @@ -176,7 +176,7 @@ private static String toQualifiedClassName(JavaFileObject file) { private static final Pattern PACKAGE_DECLARATION_PATTERN = Pattern.compile("^package\\s+(\\S+)\\s*;", Pattern.MULTILINE); private static final Pattern CLASS_DECLARATION_PATTERN = Pattern.compile( - "^public\\s+(?:class|interface)\\s+(\\S+)\\s*(?:extends\\s+\\S+)?\\s*(?:implements\\s+\\S+(?:\\s*,\\s*\\S+)*)?\\s*\\{", + "^public\\s+(?:abstract\\s+)?(?:class|@?interface)\\s+(\\S+)\\s*(?:extends\\s+\\S+)?\\s*(?:implements\\s+\\S+(?:\\s*,\\s*\\S+)*)?\\s*\\{", Pattern.MULTILINE); public JavaFileObject prepareSourceFile(String sourceCode) { diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactoryTest.java new file mode 100644 index 0000000..d022617 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/ElementwiseListConversionExprFactoryTest.java @@ -0,0 +1,132 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.junit.jupiter.api.Test; +import rapier.core.ConversionExprFactory; + +public class ElementwiseListConversionExprFactoryTest { + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + public void givenFullyReifiedListTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror listType = mock(TypeMirror.class); + when(listType.getKind()).thenReturn(TypeKind.DECLARED); + when(listType.toString()).thenReturn("java.util.List"); + + final TypeMirror targetTypeAsTypeMirror = + mock(TypeMirror.class, withSettings().extraInterfaces(DeclaredType.class)); + when(targetTypeAsTypeMirror.getKind()).thenReturn(TypeKind.DECLARED); + when(targetTypeAsTypeMirror.toString()).thenReturn("java.util.List"); + when(types.erasure(targetTypeAsTypeMirror)).thenReturn(listType); + + final TypeMirror targetTypeTypeArgument = mock(TypeMirror.class); + when(targetTypeTypeArgument.getKind()).thenReturn(TypeKind.DECLARED); + when(targetTypeTypeArgument.toString()).thenReturn("java.lang.Integer"); + + final List targetTypeTypeArguments = new ArrayList<>(); + targetTypeTypeArguments.add(targetTypeTypeArgument); + + final DeclaredType targetTypeAsDeclaredType = (DeclaredType) targetTypeAsTypeMirror; + when(targetTypeAsDeclaredType.getTypeArguments()).thenReturn((List) targetTypeTypeArguments); + + final ConversionExprFactory innerConversionExprFactory = mock(ConversionExprFactory.class); + when(innerConversionExprFactory.generateConversionExpr(targetTypeTypeArgument, "element")) + .thenReturn(Optional.of("Integer.valueOf(element)")); + + final ElementwiseListConversionExprFactory unit = + new ElementwiseListConversionExprFactory(types, innerConversionExprFactory); + + final String conversionExpr = + unit.generateConversionExpr(targetTypeAsTypeMirror, "value").orElse(null); + + assertEquals( + "value.stream().map(element -> Integer.valueOf(element)).collect(java.util.stream.Collectors.toList())", + conversionExpr); + } + + @Test + public void givenRawListTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetTypeAsTypeMirror = + mock(TypeMirror.class, withSettings().extraInterfaces(DeclaredType.class)); + when(targetTypeAsTypeMirror.getKind()).thenReturn(TypeKind.DECLARED); + when(targetTypeAsTypeMirror.toString()).thenReturn("java.util.List"); + when(types.erasure(targetTypeAsTypeMirror)).thenReturn(targetTypeAsTypeMirror); + + final DeclaredType targetTypeAsDeclaredType = (DeclaredType) targetTypeAsTypeMirror; + when(targetTypeAsDeclaredType.getTypeArguments()).thenReturn(emptyList()); + + final ConversionExprFactory innerConversionExprFactory = mock(ConversionExprFactory.class); + + final ElementwiseListConversionExprFactory unit = + new ElementwiseListConversionExprFactory(types, innerConversionExprFactory); + + final String conversionExpr = + unit.generateConversionExpr(targetTypeAsTypeMirror, "value").orElse(null); + + assertEquals(null, conversionExpr); + + verifyNoInteractions(innerConversionExprFactory); + } + + @Test + public void givenNonListTargetType_whenComputeConversionExpr_thenGetNoConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror mapType = mock(TypeMirror.class); + when(mapType.getKind()).thenReturn(TypeKind.DECLARED); + when(mapType.toString()).thenReturn("java.util.Map"); + + final TypeMirror targetTypeAsTypeMirror = + mock(TypeMirror.class, withSettings().extraInterfaces(DeclaredType.class)); + when(targetTypeAsTypeMirror.getKind()).thenReturn(TypeKind.DECLARED); + when(targetTypeAsTypeMirror.toString()) + .thenReturn("java.util.Map"); + when(types.erasure(targetTypeAsTypeMirror)).thenReturn(mapType); + + final ConversionExprFactory innerConversionExprFactory = mock(ConversionExprFactory.class); + + final ElementwiseListConversionExprFactory unit = + new ElementwiseListConversionExprFactory(types, innerConversionExprFactory); + + final String conversionExpr = + unit.generateConversionExpr(targetTypeAsTypeMirror, "value").orElse(null); + + assertEquals(null, conversionExpr); + + verifyNoInteractions(innerConversionExprFactory); + } +} diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/FromStringConversionExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/FromStringConversionExprFactoryTest.java new file mode 100644 index 0000000..f582848 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/FromStringConversionExprFactoryTest.java @@ -0,0 +1,246 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static java.util.Collections.unmodifiableList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; +import org.junit.jupiter.api.Test; +import com.google.testing.compile.Compilation; +import rapier.core.RapierTestBase; + +class FromStringConversionExprFactoryTest extends RapierTestBase { + private String conversionExpr; + + @Test + void givenClassWithMatchingFromStringMethod_whenCompile_thenGenerateExpectedConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static TestClass fromString(String value) { + return new TestClass(); + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals("com.example.TestClass.fromString(value)", conversionExpr); + } + + @Test + void givenClassWithNonMatchingParameterTypeFromStringMethod_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static TestClass fromString(Integer value) { + return new TestClass(); + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + @Test + void givenClassWithNonMatchingReturnTypeFromStringMethod_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static String fromString(String value) { + return ""; + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + @Test + void givenClassWithoutFromStringMethod_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static TestClass foobar(String value) { + return new TestClass(); + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + public static final String NO_CONVERSION_EXPR = "NONE"; + + /** + * Simple annotation processor that looks for classes annotated with {@link TestAnnotation} and + * generates a conversion expression for them. Used to test + * {@link FromStringConversionExprFactory}. + */ + @SupportedAnnotationTypes("com.example.TestAnnotation") + @SupportedSourceVersion(SourceVersion.RELEASE_11) + private class TestProcessor extends AbstractProcessor { + private ProcessingEnvironment processingEnvironment; + + @Override + public synchronized void init(ProcessingEnvironment processingEnvironment) { + this.processingEnvironment = processingEnvironment; + } + + @Override + public boolean process(Set annotations, RoundEnvironment round) { + final TypeElement annotation = getElements().getTypeElement("com.example.TestAnnotation"); + + final Set annotatedElements = round.getElementsAnnotatedWith(annotation); + for (Element annotatedElement : annotatedElements) { + if (!annotatedElement.getKind().isClass()) + continue; + conversionExpr = new FromStringConversionExprFactory(getTypes()) + .generateConversionExpr(annotatedElement.asType(), "value").orElse(NO_CONVERSION_EXPR); + } + + return true; + } + + private Elements getElements() { + return getProcessingEnvironment().getElementUtils(); + } + + private Types getTypes() { + return getProcessingEnvironment().getTypeUtils(); + } + + private ProcessingEnvironment getProcessingEnvironment() { + return processingEnvironment; + } + } + + /** + * Add our annotation processor to the list of processors to run. + */ + @Override + protected List getAnnotationProcessors() { + final List result = new ArrayList<>(super.getAnnotationProcessors()); + result.add(new TestProcessor()); + return unmodifiableList(result); + } +} diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/IdentityConversionExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/IdentityConversionExprFactoryTest.java new file mode 100644 index 0000000..f5af116 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/IdentityConversionExprFactoryTest.java @@ -0,0 +1,67 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.junit.jupiter.api.Test; + +public class IdentityConversionExprFactoryTest { + @Test + public void givenMatchingSourceAndTargetTypes_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror sourceType = mock(TypeMirror.class); + + final TypeMirror targetType = mock(TypeMirror.class); + + when(types.isSameType(targetType, sourceType)).thenReturn(true); + + final IdentityConversionExprFactory unit = + new IdentityConversionExprFactory(types, sourceType); + + final String conversionExpr = + unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("value", conversionExpr); + } + + @Test + public void givenNonMatchingSourceAndTargetTypes_whenComputeConversionExpr_thenGetNoConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror sourceType = mock(TypeMirror.class); + + final TypeMirror targetType = mock(TypeMirror.class); + + when(types.isSameType(targetType, sourceType)).thenReturn(false); + + final IdentityConversionExprFactory unit = + new IdentityConversionExprFactory(types, sourceType); + + final String conversionExpr = + unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals(null, conversionExpr); + } +} diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/SingleArgumentConstructorExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/SingleArgumentConstructorExprFactoryTest.java new file mode 100644 index 0000000..93ab4b6 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/SingleArgumentConstructorExprFactoryTest.java @@ -0,0 +1,217 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static java.util.Collections.unmodifiableList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; +import org.junit.jupiter.api.Test; +import com.google.testing.compile.Compilation; +import rapier.core.RapierTestBase; + +class SingleArgumentConstructorExprFactoryTest extends RapierTestBase { + private String conversionExpr; + + @Test + void givenClassWithMatchingSingleArgumentConstructor_whenCompile_thenGenerateExpectedConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public TestClass(Integer value) { + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals("new com.example.TestClass(value)", conversionExpr); + } + + @Test + void givenClassWithNonMatchingSingleArgumentConstructor_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public TestClass(String value) { + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + @Test + void givenAbstractClassWithMatchingSingleArgumentConstructor_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public abstract class TestClass { + public TestClass(String value) { + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + public static final String NO_CONVERSION_EXPR = "NONE"; + + /** + * Simple annotation processor that looks for classes annotated with {@link TestAnnotation} and + * generates a conversion expression for them. Used to test + * {@link FromStringConversionExprFactory}. + */ + @SupportedAnnotationTypes("com.example.TestAnnotation") + @SupportedSourceVersion(SourceVersion.RELEASE_11) + private class TestProcessor extends AbstractProcessor { + private ProcessingEnvironment processingEnvironment; + + private transient TypeMirror integerType; + + @Override + public synchronized void init(ProcessingEnvironment processingEnvironment) { + this.processingEnvironment = processingEnvironment; + this.integerType = getElements().getTypeElement("java.lang.Integer").asType(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment round) { + final TypeElement annotation = getElements().getTypeElement("com.example.TestAnnotation"); + + final Set annotatedElements = round.getElementsAnnotatedWith(annotation); + for (Element annotatedElement : annotatedElements) { + if (!annotatedElement.getKind().isClass()) + continue; + conversionExpr = + new SingleArgumentConstructorConversionExprFactory(getTypes(), getIntegerType()) + .generateConversionExpr(annotatedElement.asType(), "value") + .orElse(NO_CONVERSION_EXPR); + } + + return true; + } + + private Elements getElements() { + return getProcessingEnvironment().getElementUtils(); + } + + private Types getTypes() { + return getProcessingEnvironment().getTypeUtils(); + } + + private ProcessingEnvironment getProcessingEnvironment() { + return processingEnvironment; + } + + private TypeMirror getIntegerType() { + return integerType; + } + } + + /** + * Add our annotation processor to the list of processors to run. + */ + @Override + protected List getAnnotationProcessors() { + final List result = new ArrayList<>(super.getAnnotationProcessors()); + result.add(new TestProcessor()); + return unmodifiableList(result); + } +} diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/StringToCharacterExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/StringToCharacterExprFactoryTest.java new file mode 100644 index 0000000..3a35956 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/StringToCharacterExprFactoryTest.java @@ -0,0 +1,61 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.junit.jupiter.api.Test; + +public class StringToCharacterExprFactoryTest { + @Test + public void givenCharacterTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.toString()).thenReturn("java.lang.Character"); + + final StringToCharacterConversionExprFactory unit = + new StringToCharacterConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals( + "Optional.of(value).map(s -> s.isEmpty() ? null : s.charAt(0)).orElseThrow(() -> new IllegalStateException(\"Cannot convert empty string to char\"))", + conversionExpr); + } + + @Test + public void givenNonCharacterTargetType_whenComputeConversionExpr_thenGetNoConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.toString()).thenReturn("java.lang.Integer"); + + final StringToCharacterConversionExprFactory unit = + new StringToCharacterConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals(null, conversionExpr); + } +} diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/StringToPrimitiveExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/StringToPrimitiveExprFactoryTest.java new file mode 100644 index 0000000..484aa95 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/StringToPrimitiveExprFactoryTest.java @@ -0,0 +1,167 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import org.junit.jupiter.api.Test; + +public class StringToPrimitiveExprFactoryTest { + @Test + public void givenByteTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.BYTE); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Byte.parseByte(value)", conversionExpr); + } + + @Test + public void givenShortTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.SHORT); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Short.parseShort(value)", conversionExpr); + } + + @Test + public void givenIntTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.INT); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Integer.parseInt(value)", conversionExpr); + } + + @Test + public void givenLongTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.LONG); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Long.parseLong(value)", conversionExpr); + } + + @Test + public void givenCharTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.CHAR); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals( + "Optional.of(value).map(s -> s.isEmpty() ? null : s.charAt(0)).orElseThrow(() -> new IllegalStateException(\"Cannot convert empty string to char\"))", + conversionExpr); + } + + @Test + public void givenFloatTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.FLOAT); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Float.parseFloat(value)", conversionExpr); + } + + @Test + public void givenDoubleTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.DOUBLE); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Double.parseDouble(value)", conversionExpr); + } + + @Test + public void givenBooleanTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.BOOLEAN); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals("Boolean.parseBoolean(value)", conversionExpr); + } + + @Test + public void givenNonPrimitiveTargetType_whenComputeConversionExpr_thenGetExpectedConversionExpr() { + final Types types = mock(Types.class); + + final TypeMirror targetType = mock(TypeMirror.class); + when(targetType.getKind()).thenReturn(TypeKind.DECLARED); + + final StringToPrimitiveConversionExprFactory unit = + new StringToPrimitiveConversionExprFactory(types); + + final String conversionExpr = unit.generateConversionExpr(targetType, "value").orElse(null); + + assertEquals(null, conversionExpr); + } +} diff --git a/rapier-core/src/test/java/rapier/core/conversion/expr/ValueOfConversionExprFactoryTest.java b/rapier-core/src/test/java/rapier/core/conversion/expr/ValueOfConversionExprFactoryTest.java new file mode 100644 index 0000000..2254b81 --- /dev/null +++ b/rapier-core/src/test/java/rapier/core/conversion/expr/ValueOfConversionExprFactoryTest.java @@ -0,0 +1,254 @@ +/*- + * =================================LICENSE_START================================== + * rapier-core + * ====================================SECTION===================================== + * Copyright (C) 2024 - 2025 Andy Boothe + * ====================================SECTION===================================== + * 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 + * + * http://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. + * ==================================LICENSE_END=================================== + */ +package rapier.core.conversion.expr; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static java.util.Collections.unmodifiableList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; +import org.junit.jupiter.api.Test; +import com.google.testing.compile.Compilation; +import rapier.core.RapierTestBase; + +class ValueOfConversionExprFactoryTest extends RapierTestBase { + private String conversionExpr; + + @Test + void givenClassWithMatchingValueOfMethod_whenCompile_thenGenerateExpectedConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static TestClass valueOf(Integer value) { + return new TestClass(); + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals("com.example.TestClass.valueOf(value)", conversionExpr); + } + + @Test + void givenClassWithNonMatchingParameterTypeValueOfMethod_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static TestClass valueOf(String value) { + return new TestClass(); + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + @Test + void givenClassWithNonMatchingReturnTypeValueOfMethod_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static String valueOf(Integer value) { + return ""; + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + @Test + void givenClassWithoutValueOfMethod_whenCompile_thenGenerateNoConversionExpr() + throws IOException { + final JavaFileObject testAnnotationSource = prepareSourceFile(""" + package com.example; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + public @interface TestAnnotation { + } + """); + + final JavaFileObject testClassSource = prepareSourceFile(""" + package com.example; + + @com.example.TestAnnotation + public class TestClass { + public static TestClass foobar(Integer value) { + return new TestClass(); + } + } + """); + + // Compile the test class + Compilation compilation = doCompile(testAnnotationSource, testClassSource); + + assertThat(compilation).succeeded(); + + assertEquals(NO_CONVERSION_EXPR, conversionExpr); + } + + public static final String NO_CONVERSION_EXPR = "NONE"; + + /** + * Simple annotation processor that looks for classes annotated with {@link TestAnnotation} and + * generates a conversion expression for them. Used to test + * {@link FromStringConversionExprFactory}. + */ + @SupportedAnnotationTypes("com.example.TestAnnotation") + @SupportedSourceVersion(SourceVersion.RELEASE_11) + private class TestProcessor extends AbstractProcessor { + private ProcessingEnvironment processingEnvironment; + + private transient TypeMirror integerType; + + @Override + public synchronized void init(ProcessingEnvironment processingEnvironment) { + this.processingEnvironment = processingEnvironment; + this.integerType = getElements().getTypeElement("java.lang.Integer").asType(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment round) { + final TypeElement annotation = getElements().getTypeElement("com.example.TestAnnotation"); + + final Set annotatedElements = round.getElementsAnnotatedWith(annotation); + for (Element annotatedElement : annotatedElements) { + if (!annotatedElement.getKind().isClass()) + continue; + conversionExpr = new ValueOfConversionExprFactory(getTypes(), getIntegerType()) + .generateConversionExpr(annotatedElement.asType(), "value").orElse(NO_CONVERSION_EXPR); + } + + return true; + } + + private Elements getElements() { + return getProcessingEnvironment().getElementUtils(); + } + + private Types getTypes() { + return getProcessingEnvironment().getTypeUtils(); + } + + private ProcessingEnvironment getProcessingEnvironment() { + return processingEnvironment; + } + + private TypeMirror getIntegerType() { + return integerType; + } + } + + /** + * Add our annotation processor to the list of processors to run. + */ + @Override + protected List getAnnotationProcessors() { + final List result = new ArrayList<>(super.getAnnotationProcessors()); + result.add(new TestProcessor()); + return unmodifiableList(result); + } +}