1 /*
2  * Copyright (C) 2023 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 com.android.settingslib.tools.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.Issue
24 import com.android.tools.lint.detector.api.JavaContext
25 import com.android.tools.lint.detector.api.LintFix
26 import com.android.tools.lint.detector.api.Scope
27 import com.android.tools.lint.detector.api.Severity
28 import com.intellij.psi.PsiModifier
29 import com.intellij.psi.PsiPrimitiveType
30 import com.intellij.psi.PsiType
31 import org.jetbrains.uast.UAnnotated
32 import org.jetbrains.uast.UElement
33 import org.jetbrains.uast.UMethod
34 
35 class NullabilityAnnotationsDetector : Detector(), Detector.UastScanner {
getApplicableUastTypesnull36     override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UMethod::class.java)
37 
38     override fun createUastHandler(context: JavaContext): UElementHandler? {
39         if (!context.isJavaFile()) return null
40 
41         return object : UElementHandler() {
42             override fun visitMethod(node: UMethod) {
43                 if (node.isPublic() && node.name != ANONYMOUS_CONSTRUCTOR) {
44                     node.verifyMethod()
45                     node.verifyMethodParameters()
46                 }
47             }
48 
49             private fun UMethod.isPublic() = modifierList.hasModifierProperty(PsiModifier.PUBLIC)
50 
51             private fun UMethod.verifyMethod() {
52                 if (isConstructor) return
53                 if (returnType.isPrimitive()) return
54                 checkAnnotation(METHOD_MSG)
55             }
56 
57             private fun UMethod.verifyMethodParameters() {
58                 for (parameter in uastParameters) {
59                     if (parameter.type.isPrimitive()) continue
60                     parameter.checkAnnotation(PARAMETER_MSG)
61                 }
62             }
63 
64             private fun PsiType?.isPrimitive() = this is PsiPrimitiveType
65 
66             private fun UAnnotated.checkAnnotation(message: String) {
67                 val oldAnnotation = findOldNullabilityAnnotation()
68                 val oldAnnotationName = oldAnnotation?.qualifiedName?.substringAfterLast('.')
69 
70                 if (oldAnnotationName != null) {
71                     val annotation = "androidx.annotation.$oldAnnotationName"
72                     reportIssue(
73                         REQUIRE_NULLABILITY_ISSUE,
74                         "Prefer $annotation",
75                         LintFix.create()
76                                 .replace()
77                                 .range(context.getLocation(oldAnnotation))
78                                 .with("@$annotation")
79                                 .autoFix()
80                                 .build()
81                     )
82                 } else if (!hasNullabilityAnnotation()) {
83                     reportIssue(REQUIRE_NULLABILITY_ISSUE, message)
84                 }
85             }
86 
87             private fun UElement.reportIssue(
88                 issue: Issue,
89                 message: String,
90                 quickfixData: LintFix? = null,
91             ) {
92                 context.report(
93                     issue = issue,
94                     scope = this,
95                     location = context.getNameLocation(this),
96                     message = message,
97                     quickfixData = quickfixData,
98                 )
99             }
100 
101             private fun UAnnotated.findOldNullabilityAnnotation() =
102                 uAnnotations.find { it.qualifiedName in oldAnnotations }
103 
104             private fun UAnnotated.hasNullabilityAnnotation() =
105                 uAnnotations.any { it.qualifiedName in validAnnotations }
106         }
107     }
108 
JavaContextnull109     private fun JavaContext.isJavaFile() = psiFile?.fileElementType.toString().startsWith("java")
110 
111     companion object {
112         private val validAnnotations = arrayOf("androidx.annotation.NonNull",
113             "androidx.annotation.Nullable")
114 
115         private val oldAnnotations = arrayOf("android.annotation.NonNull",
116             "android.annotation.Nullable",
117         )
118 
119         private const val ANONYMOUS_CONSTRUCTOR = "<anon-init>"
120 
121         private const val METHOD_MSG =
122                 "Java public method return with non-primitive type must add androidx annotation. " +
123                         "Example: @NonNull | @Nullable Object functionName() {}"
124 
125         private const val PARAMETER_MSG =
126                 "Java public method parameter with non-primitive type must add androidx " +
127                         "annotation. Example: functionName(@NonNull Context context, " +
128                         "@Nullable Object obj) {}"
129 
130         internal val REQUIRE_NULLABILITY_ISSUE = Issue
131             .create(
132                 id = "RequiresNullabilityAnnotation",
133                 briefDescription = "Requires nullability annotation for function",
134                 explanation = "All public java APIs should specify nullability annotations for " +
135                         "methods and parameters.",
136                 category = Category.CUSTOM_LINT_CHECKS,
137                 priority = 3,
138                 severity = Severity.WARNING,
139                 androidSpecific = true,
140                 implementation = Implementation(
141                   NullabilityAnnotationsDetector::class.java,
142                   Scope.JAVA_FILE_SCOPE,
143                 ),
144             )
145     }
146 }