Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7c382e74 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add a linter requiring Nullability Annotations" into main

parents cde96e6b 7b48eb34
Loading
Loading
Loading
Loading
+33 −0
Original line number Diff line number Diff line
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package {
    // See: http://go/android-license-faq
    // A large-scale-change added 'default_applicable_licenses' to import
    // all of the 'license_kinds' from "frameworks_base_license"
    // to get the below license kinds:
    //   SPDX-license-identifier-Apache-2.0
    default_applicable_licenses: ["frameworks_base_license"],
}

java_library_host {
    name: "SettingsLibLintChecker",
    srcs: ["src/**/*.kt"],
    plugins: ["auto_service_plugin"],
    libs: [
        "auto_service_annotations",
        "lint_api",
    ],
    kotlincflags: ["-Xjvm-default=all"],
}
+146 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.tools.lint

import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiPrimitiveType
import com.intellij.psi.PsiType
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UMethod

class NullabilityAnnotationsDetector : Detector(), Detector.UastScanner {
    override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UMethod::class.java)

    override fun createUastHandler(context: JavaContext): UElementHandler? {
        if (!context.isJavaFile()) return null

        return object : UElementHandler() {
            override fun visitMethod(node: UMethod) {
                if (node.isPublic() && node.name != ANONYMOUS_CONSTRUCTOR) {
                    node.verifyMethod()
                    node.verifyMethodParameters()
                }
            }

            private fun UMethod.isPublic() = modifierList.hasModifierProperty(PsiModifier.PUBLIC)

            private fun UMethod.verifyMethod() {
                if (isConstructor) return
                if (returnType.isPrimitive()) return
                checkAnnotation(METHOD_MSG)
            }

            private fun UMethod.verifyMethodParameters() {
                for (parameter in uastParameters) {
                    if (parameter.type.isPrimitive()) continue
                    parameter.checkAnnotation(PARAMETER_MSG)
                }
            }

            private fun PsiType?.isPrimitive() = this is PsiPrimitiveType

            private fun UAnnotated.checkAnnotation(message: String) {
                val oldAnnotation = findOldNullabilityAnnotation()
                val oldAnnotationName = oldAnnotation?.qualifiedName?.substringAfterLast('.')

                if (oldAnnotationName != null) {
                    val annotation = "androidx.annotation.$oldAnnotationName"
                    reportIssue(
                        REQUIRE_NULLABILITY_ISSUE,
                        "Prefer $annotation",
                        LintFix.create()
                                .replace()
                                .range(context.getLocation(oldAnnotation))
                                .with("@$annotation")
                                .autoFix()
                                .build()
                    )
                } else if (!hasNullabilityAnnotation()) {
                    reportIssue(REQUIRE_NULLABILITY_ISSUE, message)
                }
            }

            private fun UElement.reportIssue(
                issue: Issue,
                message: String,
                quickfixData: LintFix? = null,
            ) {
                context.report(
                    issue = issue,
                    scope = this,
                    location = context.getNameLocation(this),
                    message = message,
                    quickfixData = quickfixData,
                )
            }

            private fun UAnnotated.findOldNullabilityAnnotation() =
                uAnnotations.find { it.qualifiedName in oldAnnotations }

            private fun UAnnotated.hasNullabilityAnnotation() =
                uAnnotations.any { it.qualifiedName in validAnnotations }
        }
    }

    private fun JavaContext.isJavaFile() = psiFile?.fileElementType.toString().startsWith("java")

    companion object {
        private val validAnnotations = arrayOf("androidx.annotation.NonNull",
            "androidx.annotation.Nullable")

        private val oldAnnotations = arrayOf("android.annotation.NonNull",
            "android.annotation.Nullable",
        )

        private const val ANONYMOUS_CONSTRUCTOR = "<anon-init>"

        private const val METHOD_MSG =
                "Java public method return with non-primitive type must add androidx annotation. " +
                        "Example: @NonNull | @Nullable Object functionName() {}"

        private const val PARAMETER_MSG =
                "Java public method parameter with non-primitive type must add androidx " +
                        "annotation. Example: functionName(@NonNull Context context, " +
                        "@Nullable Object obj) {}"

        internal val REQUIRE_NULLABILITY_ISSUE = Issue
            .create(
                id = "RequiresNullabilityAnnotation",
                briefDescription = "Requires nullability annotation for function",
                explanation = "All public java APIs should specify nullability annotations for " +
                        "methods and parameters.",
                category = Category.CUSTOM_LINT_CHECKS,
                priority = 3,
                severity = Severity.WARNING,
                androidSpecific = true,
                implementation = Implementation(
                  NullabilityAnnotationsDetector::class.java,
                  Scope.JAVA_FILE_SCOPE,
                ),
            )
    }
}
 No newline at end of file
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.tools.lint

import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.google.auto.service.AutoService

@AutoService(IssueRegistry::class)
class SettingsLintIssueRegistry : IssueRegistry() {
    override val issues = listOf(NullabilityAnnotationsDetector.REQUIRE_NULLABILITY_ISSUE)

    override val api: Int = CURRENT_API
}
 No newline at end of file