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