/* * Copyright (C) 2021 The Dagger 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 * * 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. */ package dagger.internal.codegen; import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; import static org.junit.Assert.assertThrows; import androidx.room.compiler.processing.XConstructorElement; import androidx.room.compiler.processing.XMethodElement; import androidx.room.compiler.processing.XProcessingEnv; import androidx.room.compiler.processing.XTypeElement; import androidx.room.compiler.processing.XVariableElement; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.testing.compile.JavaFileObjects; import dagger.Component; import dagger.internal.codegen.base.DaggerSuperficialValidation; import dagger.internal.codegen.base.DaggerSuperficialValidation.ValidationException; import dagger.internal.codegen.javac.JavacPluginModule; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.inject.Singleton; import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class DaggerSuperficialValidationTest { private static final Joiner NEW_LINES = Joiner.on("\n "); @Test public void missingReturnType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "abstract class TestClass {", " abstract MissingType blah();", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (METHOD): blah()", " => type (EXECUTABLE method): ()MissingType", " => type (ERROR return type): MissingType")); } }) .failsToCompile(); } @Test public void missingGenericReturnType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "abstract class TestClass {", " abstract MissingType blah();", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (METHOD): blah()", " => type (EXECUTABLE method): ()", " => type (ERROR return type): ")); } }) .failsToCompile(); } @Test public void missingReturnTypeTypeParameter() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import java.util.Map;", "import java.util.Set;", "", "abstract class TestClass {", " abstract Map, MissingType> blah();", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (METHOD): blah()", " => type (EXECUTABLE method): " + "()java.util.Map,>", " => type (DECLARED return type): " + "java.util.Map,>", " => type (ERROR type argument): ")); } }) .failsToCompile(); } @Test public void missingTypeParameter() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", // "package test;", "", "class TestClass {}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (TYPE_PARAMETER): T", " => type (ERROR bound type): MissingType")); } }) .failsToCompile(); } @Test public void missingParameterType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "abstract class TestClass {", " abstract void foo(MissingType x);", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (METHOD): foo(MissingType)", " => type (EXECUTABLE method): (MissingType)void", " => type (ERROR parameter type): MissingType")); } }) .failsToCompile(); } @Test public void missingAnnotation() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", // "package test;", "", "@MissingAnnotation", "class TestClass {}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => annotation: @MissingAnnotation", " => type (ERROR annotation type): MissingAnnotation")); } }) .failsToCompile(); } @Test public void handlesRecursiveTypeParams() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", // "package test;", "", "class TestClass> {}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); superficialValidation.validateElement(testClassElement); } }) .compilesWithoutError(); } @Test public void handlesRecursiveType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "abstract class TestClass {", " abstract TestClass foo(TestClass x);", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); superficialValidation.validateElement(testClassElement); } }) .compilesWithoutError(); } @Test public void missingWildcardBound() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "import java.util.Set;", "", "class TestClass {", " Set extendsTest() {", " return null;", " }", "", " Set superTest() {", " return null;", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (METHOD): extendsTest()", " => type (EXECUTABLE method): ()java.util.Set", " => type (DECLARED return type): " + "java.util.Set", " => type (WILDCARD type argument): ? extends MissingType", " => type (ERROR extends bound type): MissingType")); } }) .failsToCompile(); } @Test public void missingIntersection() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.TestClass", "package test;", "", "class TestClass {}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.TestClass", " => element (TYPE_PARAMETER): T", " => type (ERROR bound type): Missing")); } }) .failsToCompile(); } @Test public void invalidAnnotationValue() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "final class Outer {", " @interface TestAnnotation {", " Class[] classes();", " }", "", " @TestAnnotation(classes = Foo)", " static class TestClass {}", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.Outer.TestClass"); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(testClassElement)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.Outer.TestClass", " => annotation: @test.Outer.TestAnnotation(classes = \"\")", " => annotation method: java.lang.Class[] classes()", " => annotation value (ARRAY): value '' with expected type" + " java.lang.Class[]", " => annotation value (STRING): value '' with expected type" + " java.lang.Class")); } }) .failsToCompile(); } @Test public void invalidAnnotationValueOnParameter() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "final class Outer {", " @interface TestAnnotation {", " Class[] classes();", " }", "", " static class TestClass {", " TestClass(@TestAnnotation(classes = Foo) String strParam) {}", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement testClassElement = processingEnv.findTypeElement("test.Outer.TestClass"); XConstructorElement constructor = testClassElement.getConstructors().get(0); XVariableElement parameter = constructor.getParameters().get(0); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateElement(parameter)); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.Outer.TestClass", " => element (CONSTRUCTOR): TestClass(java.lang.String)", " => element (PARAMETER): strParam", " => annotation: @test.Outer.TestAnnotation(classes = \"\")", " => annotation method: java.lang.Class[] classes()", " => annotation value (ARRAY): value '' with expected type" + " java.lang.Class[]", " => annotation value (STRING): value '' with expected type" + " java.lang.Class")); } }) .failsToCompile(); } @Test public void invalidSuperclassInTypeHierarchy() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "final class Outer {", " Child getChild() { return null; }", "", " static class Child extends Parent {}", "", " static class Parent extends MissingType {}", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement outerElement = processingEnv.findTypeElement("test.Outer"); XMethodElement getChildMethod = outerElement.getDeclaredMethods().get(0); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateTypeHierarchyOf( "return type", getChildMethod, getChildMethod.getReturnType())); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.Outer", " => element (METHOD): getChild()", " => type (DECLARED return type): test.Outer.Child", " => type (DECLARED supertype): test.Outer.Parent", " => type (ERROR supertype): MissingType")); } }) .failsToCompile(); } @Test public void invalidSuperclassTypeParameterInTypeHierarchy() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "test.Outer", "package test;", "", "final class Outer {", " Child getChild() { return null; }", "", " static class Child extends Parent {}", "", " static class Parent {}", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith( new AssertingProcessor() { @Override void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) { XTypeElement outerElement = processingEnv.findTypeElement("test.Outer"); XMethodElement getChildMethod = outerElement.getDeclaredMethods().get(0); ValidationException exception = assertThrows( ValidationException.KnownErrorType.class, () -> superficialValidation.validateTypeHierarchyOf( "return type", getChildMethod, getChildMethod.getReturnType())); assertThat(exception) .hasMessageThat() .contains( NEW_LINES.join( "Validation trace:", " => element (CLASS): test.Outer", " => element (METHOD): getChild()", " => type (DECLARED return type): test.Outer.Child", " => type (DECLARED supertype): test.Outer.Parent", " => type (ERROR type argument): MissingType")); } }) .failsToCompile(); } private abstract static class AssertingProcessor extends AbstractProcessor { private boolean processed = false; @Override public Set getSupportedAnnotationTypes() { return ImmutableSet.of("*"); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { if (!processed) { processed = true; // only process once. TestComponent component = DaggerDaggerSuperficialValidationTest_TestComponent.builder() .javacPluginModule( new JavacPluginModule( processingEnv.getElementUtils(), processingEnv.getTypeUtils())) .build(); try { runAssertions(component.processingEnv(), component.superficialValidation()); } catch (Exception e) { throw new RuntimeException(e); } } return false; } abstract void runAssertions( XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation) throws Exception; } @Singleton @Component(modules = JavacPluginModule.class) interface TestComponent { XProcessingEnv processingEnv(); DaggerSuperficialValidation superficialValidation(); } }