1 /* 2 * Copyright 2021 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.auto.value.processor; 17 18 import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION; 19 import static com.google.common.truth.OptionalSubject.optionals; 20 import static com.google.common.truth.Truth.assertThat; 21 import static com.google.common.truth.TruthJUnit.assume; 22 import static com.google.testing.compile.CompilationSubject.assertThat; 23 import static java.util.stream.Collectors.partitioningBy; 24 25 import com.google.auto.common.MoreTypes; 26 import com.google.common.collect.ImmutableList; 27 import com.google.common.truth.Expect; 28 import com.google.testing.compile.Compilation; 29 import com.google.testing.compile.Compiler; 30 import com.google.testing.compile.JavaFileObjects; 31 import java.lang.annotation.ElementType; 32 import java.lang.annotation.Retention; 33 import java.lang.annotation.RetentionPolicy; 34 import java.lang.annotation.Target; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Set; 38 import javax.annotation.processing.AbstractProcessor; 39 import javax.annotation.processing.RoundEnvironment; 40 import javax.annotation.processing.SupportedAnnotationTypes; 41 import javax.lang.model.SourceVersion; 42 import javax.lang.model.element.AnnotationMirror; 43 import javax.lang.model.element.ExecutableElement; 44 import javax.lang.model.element.TypeElement; 45 import javax.lang.model.type.DeclaredType; 46 import javax.lang.model.util.ElementFilter; 47 import org.junit.Rule; 48 import org.junit.Test; 49 import org.junit.runner.RunWith; 50 import org.junit.runners.JUnit4; 51 52 @RunWith(JUnit4.class) 53 public class NullablesTest { 54 @Rule public Expect expect = Expect.create(); 55 56 @Target(ElementType.TYPE_USE) 57 @Retention(RetentionPolicy.RUNTIME) 58 public @interface Nullable {} 59 60 @Target(ElementType.TYPE_USE) 61 @Retention(RetentionPolicy.RUNTIME) 62 public @interface Irrelevant {} 63 64 // The class here has various methods that we will examine with 65 // Nullables.nullableMentionedInMethods to ensure that we do indeed detect @Nullable annotations 66 // in various contexts. 67 // This test is a lot more complicated than it should be. Ideally we would just have this Methods 68 // class be an actual class nested inside this test, and we would use CompilationRule to get 69 // the corresponding TypeElement so we could check the various methods. Unfortunately, if we 70 // do that then we get a TypeElement where all type annotations have disappeared because of 71 // https://bugs.openjdk.java.net/browse/JDK-8225377. So instead we have to use Compiler to compile 72 // the code here with a special annotation processor that will check the annotations of the 73 // just-compiled class. Since the processor is running as part of the same compilation, we don't 74 // lose the type annotations. 75 76 private static final ImmutableList<String> METHOD_LINES = 77 ImmutableList.of( 78 // Methods in this class whose names begin with "no" do not mention @Nullable anywhere.", 79 // All other methods do.", 80 "package foo.bar;", 81 "", 82 "import " + Irrelevant.class.getCanonicalName() + ";", 83 "import " + Nullable.class.getCanonicalName() + ";", 84 "import java.util.List;", 85 "", 86 "abstract class Methods {", 87 " void noAnnotations() {}", 88 " abstract int noAnnotationsEither(int x);", 89 " abstract @Irrelevant String noRelevantAnnotations(@Irrelevant int x);", 90 " abstract @Nullable String nullableString();", 91 " abstract String @Nullable [] nullableArrayOfString();", 92 " abstract @Nullable String[] arrayOfNullableString();", 93 " abstract @Nullable String @Nullable [] nullableArrayOfNullableString();", 94 " abstract List<@Nullable String> listOfNullableString();", 95 " abstract List<? extends @Nullable Object> listOfExtendsNullable();", 96 " abstract List<? super @Nullable Number> listOfSuperNullable();", 97 " abstract <T extends @Nullable Object> T nullableTypeParamBound();", 98 " abstract <T> @Nullable T nullableTypeParamRef();", 99 " void nullableParam(@Nullable String x) {}", 100 " void nullableParamBound(List<? extends @Nullable String> x) {}", 101 "}"); 102 103 @Test nullableMentionedInMethods()104 public void nullableMentionedInMethods() { 105 // Sadly we can't rely on JDK 8 to handle type annotations correctly. 106 // Some versions do, some don't. So skip the test unless we are on at least JDK 9. 107 double javaVersion = Double.parseDouble(JAVA_SPECIFICATION_VERSION.value()); 108 assume().that(javaVersion).isAtLeast(9.0); 109 NullableProcessor processor = new NullableProcessor(expect); 110 Compilation compilation = 111 Compiler.javac() 112 .withProcessors(processor) 113 .compile(JavaFileObjects.forSourceLines("foo.bar.Methods", METHOD_LINES)); 114 assertThat(compilation).succeededWithoutWarnings(); 115 assertThat(processor.ran).isTrue(); 116 // If any `expect` calls failed then the test will fail now because of the Expect rule. 117 } 118 119 @SupportedAnnotationTypes("*") 120 private static class NullableProcessor extends AbstractProcessor { 121 122 private final Expect expect; 123 boolean ran; 124 NullableProcessor(Expect expect)125 NullableProcessor(Expect expect) { 126 this.expect = expect; 127 } 128 129 @Override getSupportedSourceVersion()130 public SourceVersion getSupportedSourceVersion() { 131 return SourceVersion.latestSupported(); 132 } 133 134 @Override process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)135 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 136 if (roundEnv.processingOver()) { 137 TypeElement methodsElement = 138 processingEnv.getElementUtils().getTypeElement("foo.bar.Methods"); 139 expect.that(methodsElement).isNotNull(); 140 141 List<ExecutableElement> methods = 142 ElementFilter.methodsIn(methodsElement.getEnclosedElements()); 143 Map<Boolean, List<ExecutableElement>> partitionedMethods = 144 methods.stream() 145 .collect(partitioningBy(p -> !p.getSimpleName().toString().startsWith("no"))); 146 List<ExecutableElement> nullableMethods = partitionedMethods.get(true); 147 List<ExecutableElement> notNullableMethods = partitionedMethods.get(false); 148 149 expect 150 .about(optionals()) 151 .that(Nullables.nullableMentionedInMethods(notNullableMethods)) 152 .isEmpty(); 153 154 TypeElement nullableElement = 155 processingEnv.getElementUtils().getTypeElement(Nullable.class.getCanonicalName()); 156 expect.that(nullableElement).isNotNull(); 157 DeclaredType nullableType = MoreTypes.asDeclared(nullableElement.asType()); 158 159 for (ExecutableElement nullableMethod : nullableMethods) { 160 // Make a list with all the methods that don't have @Nullable plus one method that does. 161 ImmutableList<ExecutableElement> notNullablePlusNullable = 162 ImmutableList.<ExecutableElement>builder() 163 .addAll(notNullableMethods) 164 .add(nullableMethod) 165 .build(); 166 expect 167 .withMessage("method %s should have @Nullable", nullableMethod) 168 .about(optionals()) 169 .that( 170 Nullables.nullableMentionedInMethods(notNullablePlusNullable) 171 .map(AnnotationMirror::getAnnotationType)) 172 .hasValue(nullableType); 173 } 174 ran = true; 175 } 176 return false; 177 } 178 } 179 } 180