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 }