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