1 /*
2  * 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.LintFix
27 import com.android.tools.lint.detector.api.Location
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import com.android.tools.lint.detector.api.isKotlin
31 import com.intellij.psi.PsiArrayType
32 import com.intellij.psi.PsiClassType
33 import com.intellij.psi.PsiEllipsisType
34 import com.intellij.psi.PsiPrimitiveType
35 import java.util.EnumSet
36 import org.jetbrains.uast.UAnnotation
37 import org.jetbrains.uast.UElement
38 import org.jetbrains.uast.UField
39 import org.jetbrains.uast.ULocalVariable
40 import org.jetbrains.uast.UMethod
41 import org.jetbrains.uast.UParameter
42 
43 /**
44  * Repositions nullness annotations to facilitate migrating the nullness annotations to JSpecify
45  * TYPE_USE annotations. See the issue description in the companion object for more detail.
46  */
47 class JSpecifyNullnessMigration : Detector(), Detector.UastScanner {
getApplicableUastTypesnull48     override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
49 
50     override fun createUastHandler(context: JavaContext): UElementHandler {
51         return AnnotationChecker(context)
52     }
53 
54     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
visitAnnotationnull55         override fun visitAnnotation(node: UAnnotation) {
56             // Nullness annotations are only relevant for Java source.
57             if (isKotlin(node.lang)) return
58 
59             // Verify this is a nullness annotation.
60             val annotationName = node.qualifiedName ?: return
61             if (annotationName !in nullnessAnnotations.keys) return
62             val replacementAnnotationName = nullnessAnnotations[annotationName]!!
63 
64             val fix = createFix(node, replacementAnnotationName)
65             val incident =
66                 Incident(context)
67                     .message("Switch nullness annotation to JSpecify")
68                     .issue(ISSUE)
69                     .location(context.getLocation(node as UElement))
70                     .scope(node)
71                     .fix(fix)
72             context.report(incident)
73         }
74 
createFixnull75         fun createFix(node: UAnnotation, replacementAnnotationName: String): LintFix? {
76             // Find the type of the annotated element.
77             val annotated = node.uastParent ?: return null
78             val type =
79                 when (annotated) {
80                     is UParameter -> annotated.type
81                     is UMethod -> annotated.returnType
82                     is UField -> annotated.type
83                     is ULocalVariable -> annotated.type
84                     else -> return null
85                 } ?: return null
86 
87             // Determine the file location for the autofix. This is a bit complicated because it
88             // needs to avoid editing the wrong thing, like the doc comment preceding a method, but
89             // also is doing some reformatting in the area around the annotation.
90             // This is where the annotation itself is located.
91             val annotationLocation = context.getLocation(node)
92             // This is where the element being annotated is located.
93             val annotatedLocation = context.getLocation(annotated as UElement)
94             // If the annotation and annotated element aren't on the same line, that probably means
95             // the annotation is on its own line, with indentation before it. To also get rid of
96             // that indentation, start the range at the start of the annotation's line.
97             // If the annotation and annotated element are on the same line, just start at the
98             // annotation starting place to avoid including e.g. other parameters.
99             val annotatedStart = annotatedLocation.start ?: return null
100             val annotationStart = annotationLocation.start ?: return null
101             val startLocation =
102                 if (annotatedStart.sameLine(annotationStart)) {
103                     annotationStart
104                 } else {
105                     Location.create(
106                             context.file,
107                             context.getContents()!!.toString(),
108                             annotationStart.line
109                         )
110                         .start!!
111                 }
112             val fixLocation =
113                 Location.create(annotatedLocation.file, startLocation, annotatedLocation.end)
114 
115             // Part 1 of the fix: remove the original annotation
116             val annotationString = node.asSourceString()
117             val removeOriginalAnnotation =
118                 fix()
119                     .replace()
120                     .range(fixLocation)
121                     // In addition to the annotation, also remove any extra whitespace and trailing
122                     // new line. The reformat option unfortunately doesn't do this.
123                     .pattern("((    )*$annotationString ?\n?)")
124                     .with("")
125                     // Only remove one instance of the annotation.
126                     .repeatedly(false)
127                     .autoFix()
128                     .build()
129 
130             // The jspecify annotations can't be applied to primitive types (since primitives are
131             // non-null by definition) or local variables, so just remove the annotation in those
132             // cases. For all other cases, also add a new annotation to the correct position.
133             return if (type is PsiPrimitiveType || annotated is ULocalVariable) {
134                 removeOriginalAnnotation
135             } else {
136                 // Create a regex pattern for where to insert the annotation. The replacement lint
137                 // removes the first capture group (section in parentheses) of the supplied regex.
138                 // Since this fix is really just to insert an annotation, use an empty capture group
139                 // so nothing is removed.
140                 val (prefix, textToReplace) =
141                     when {
142                         // For a vararg type where the component type is an array, the annotation
143                         // goes before the array instead of the vararg ("String @NonNull []..."),
144                         // so only match the "..." when the component isn't an array.
145                         type is PsiEllipsisType && type.componentType !is PsiArrayType ->
146                             Pair(" ", "()\\.\\.\\.")
147                         type is PsiArrayType -> Pair(" ", "()\\[\\]")
148                         // Make sure to match the right usage of the class name: find the name
149                         // preceded by a space or dot, and followed by a space, open angle bracket,
150                         // or newline character.
151                         type is PsiClassType -> Pair("", "[ .]()${type.className}[ <\\n\\r]")
152                         else -> Pair("", "()${type.presentableText}")
153                     }
154                 val replacement = "$prefix@$replacementAnnotationName "
155 
156                 // Part 2 of the fix: add a new annotation.
157                 val addNewAnnotation =
158                     fix()
159                         .replace()
160                         .range(fixLocation)
161                         .pattern(textToReplace)
162                         .with(replacement)
163                         // Only add one instance of the annotation. For nested array types, this
164                         // will replace the first instance of []/..., which is correct. In
165                         // `String @Nullable [][]` the annotation applies to the outer `String[][]`
166                         // type, while in `String[] @Nullable []` it applies to the inner `String[]`
167                         // arrays.
168                         .repeatedly(false)
169                         .shortenNames()
170                         .autoFix()
171                         .build()
172 
173                 // Combine the two elements of the fix.
174                 return fix()
175                     .name("Replace annotation")
176                     .composite()
177                     .add(removeOriginalAnnotation)
178                     .add(addNewAnnotation)
179                     .autoFix()
180                     .build()
181             }
182         }
183     }
184 
185     companion object {
186         val nullnessAnnotations =
187             mapOf(
188                 "androidx.annotation.NonNull" to "org.jspecify.annotations.NonNull",
189                 "androidx.annotation.Nullable" to "org.jspecify.annotations.Nullable",
190                 "org.jetbrains.annotations.NotNull" to "org.jspecify.annotations.NonNull",
191                 "org.jetbrains.annotations.Nullable" to "org.jspecify.annotations.Nullable",
192             )
193         val ISSUE =
194             Issue.create(
195                 "JSpecifyNullness",
196                 "Migrate nullness annotations to type-use position",
197                 """
198                     Switches from AndroidX nullness annotations to JSpecify, which are type-use.
199                     Type-use annotations have different syntactic positions than non-type-use
200                     annotations in some cases.
201 
202                     For instance, when nullness annotations do not target TYPE_USE, the following
203                     definition means that the type of `arg` is nullable:
204                         @Nullable String[] arg
205                     However, if the annotation targets TYPE_USE, it now applies to the component
206                     type of the array, meaning that `arg`'s type is an array of nullable strings.
207                     To retain the original meaning, the definition needs to be changed to this:
208                         String @Nullable [] arg
209 
210                     Type-use nullness annotations must go before the simple class name of a
211                     qualified type. For instance, `java.lang.@Nullable String` is required instead
212                     of `@Nullable java.lang.String`.
213                 """,
214                 Category.CORRECTNESS,
215                 5,
216                 Severity.ERROR,
217                 Implementation(
218                     JSpecifyNullnessMigration::class.java,
219                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
220                 )
221             )
222     }
223 }
224