1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 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 17 package androidx.build.lint 18 19 import com.android.tools.lint.client.api.UElementHandler 20 import com.android.tools.lint.detector.api.Category 21 import com.android.tools.lint.detector.api.Detector 22 import com.android.tools.lint.detector.api.Implementation 23 import com.android.tools.lint.detector.api.Incident 24 import com.android.tools.lint.detector.api.Issue 25 import com.android.tools.lint.detector.api.JavaContext 26 import com.android.tools.lint.detector.api.LocationType 27 import com.android.tools.lint.detector.api.Scope 28 import com.android.tools.lint.detector.api.Severity 29 import com.android.tools.lint.detector.api.getUMethod 30 import com.android.tools.lint.detector.api.isJava 31 import com.android.tools.lint.model.LintModelMavenName 32 import com.intellij.psi.PsiMember 33 import com.intellij.psi.PsiMethod 34 import java.util.EnumSet 35 import org.jetbrains.uast.UClass 36 37 /** 38 * Enforces that the workaround for b/237064488 is applied, see issue definition for more detail. 39 */ 40 class AutoValueNullnessOverride : Detector(), Detector.UastScanner { 41 override fun getApplicableUastTypes() = listOf(UClass::class.java) 42 43 override fun createUastHandler(context: JavaContext): UElementHandler { 44 return ClassChecker(context) 45 } 46 47 private inner class ClassChecker(val context: JavaContext) : UElementHandler() { 48 override fun visitClass(node: UClass) { 49 if (!node.hasAnnotation("com.google.auto.value.AutoValue")) return 50 51 val classCoordinates = context.findMavenCoordinate(node.javaPsi) 52 53 // Narrow to the relevant methods 54 val missingOverrides = 55 node.allMethods.filter { 56 // Abstract getters are the ones used by the autovalue for builder generation 57 it.isAbstractGetter() && 58 it.isNullable() && 59 node.isSuperMethodWithoutOverride(it) && 60 isFromDifferentCompilation(it, classCoordinates) 61 } 62 63 if (missingOverrides.isEmpty()) return 64 65 // Add overrides that are just copies of the parent source code 66 val insertionText = 67 missingOverrides 68 .mapNotNull { 69 it.getUMethod()?.asSourceString()?.let { parentMethod -> 70 "\n@Override\n$parentMethod" 71 } 72 } 73 .joinToString("\n") 74 val fix = 75 if (isJava(node.language) && insertionText.isNotBlank()) { 76 fix() 77 .replace() 78 // Find the opening of the class body and insert after that 79 .pattern("\\{()") 80 .with(insertionText) 81 .reformat(true) 82 .shortenNames() 83 .range(context.getLocation(node, LocationType.ALL)) 84 .build() 85 } else { 86 null 87 } 88 89 val methodNames = missingOverrides.joinToString(", ") { "${it.name}()" } 90 val incident = 91 Incident(context) 92 .issue(ISSUE) 93 .message("Methods need @Nullable overrides for AutoValue: $methodNames") 94 .location(context.getNameLocation(node)) 95 .fix(fix) 96 97 context.report(incident) 98 } 99 100 private fun PsiMethod.isAbstractGetter() = 101 parameterList.isEmpty && modifierList.hasModifierProperty("abstract") 102 103 /** Checks if the method return type uses the JSpecify @Nullable. */ 104 private fun PsiMethod.isNullable() = 105 returnType?.hasAnnotation("org.jspecify.annotations.Nullable") == true 106 107 /** 108 * Checks that the method is defined in a different class and that the method is not also 109 * defined by a lower class in the hierarchy. 110 */ 111 private fun UClass.isSuperMethodWithoutOverride(method: PsiMethod) = 112 method.containingClass?.qualifiedName != qualifiedName && 113 // This searches starting with the class and then goes to the parent. So if it finds 114 // a matching method that isn't this method, there's an override lower down. 115 findMethodBySignature(method, true) == method 116 117 /** 118 * Checks if [member] has different maven coordinates than the [reference] coordinates, or 119 * if this is in a test context. Tests are in a different compilation from the main source 120 * but have the same maven coordinates, so to be safe always flag them. 121 */ 122 private fun isFromDifferentCompilation(member: PsiMember, reference: LintModelMavenName?) = 123 context.findMavenCoordinate(member.containingClass!!) != reference || 124 context.isTestSource 125 } 126 127 companion object { 128 val ISSUE = 129 Issue.create( 130 "AutoValueNullnessOverride", 131 "AutoValue classes must override @Nullable methods inherited from other projects", 132 """ 133 Due to a javac bug in JDK 21 and lower, AutoValue cannot see type-use nullness 134 annotations from other compilations. @AutoValue classes that inherit @Nullable 135 methods must provide an override so the AutoValue compiler doesn't make the 136 value non-null. See b/237064488 for more information. 137 """, 138 Category.CORRECTNESS, 139 5, 140 Severity.ERROR, 141 Implementation( 142 AutoValueNullnessOverride::class.java, 143 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 144 ) 145 ) 146 } 147 } 148