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

Commit 72c6685c authored by Michał Brzeziński's avatar Michał Brzeziński Committed by Android (Google) Code Review
Browse files

Merge "Adding linter for checking license (and copyright) header in java/kotlin files" into main

parents 81083c17 d552d1fb
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -214,6 +214,7 @@ android_library {

    lint: {
        extra_check_modules: ["SystemUILintChecker"],
        warning_checks: ["MissingApacheLicenseDetector"],
    },
}

+126 −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.internal.systemui.lint

import com.android.ide.common.blame.SourcePosition
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.Location
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import java.time.Year
import org.jetbrains.uast.UComment
import org.jetbrains.uast.UFile

/**
 * Checks if every AOSP Java/Kotlin source code file is starting with Apache license information.
 */
class MissingApacheLicenseDetector : Detector(), SourceCodeScanner {

    override fun getApplicableUastTypes() = listOf(UFile::class.java)

    override fun createUastHandler(context: JavaContext): UElementHandler? {
        return object : UElementHandler() {
            override fun visitFile(node: UFile) {
                val firstComment = node.allCommentsInFile.firstOrNull()
                // Normally we don't need to explicitly handle suppressing case and just return
                // error as usual with indicating node and lint will ignore it for us. But here
                // suppressing will be applied on top of comment that doesn't exist so we don't have
                // node to return - it's a bit of corner case
                if (firstComment != null && firstComment.isSuppressingComment()) {
                    return
                }
                if (firstComment == null || !firstComment.isLicenseComment()) {
                    val firstLineOfFile =
                        Location.create(
                            context.file,
                            SourcePosition(/* lineNumber= */ 1, /* column= */ 1, /* offset= */ 0)
                        )
                    context.report(
                        issue = ISSUE,
                        location = firstLineOfFile,
                        message =
                            "License header is missing\n" +
                                "Please add the following copyright and license header to the" +
                                " beginning of the file:\n\n" +
                                copyrightHeader
                    )
                }
            }
        }
    }

    private fun UComment.isSuppressingComment(): Boolean {
        val suppressingComment =
            "//noinspection ${MissingApacheLicenseDetector::class.java.simpleName}"
        return text.contains(suppressingComment)
    }

    private fun UComment.isLicenseComment(): Boolean {
        // We probably don't want to compare full copyright header in case there are some small
        // discrepancies in already existing files, e.g. year. We could do regexp but it should be
        // good enough if this detector deals with missing
        // license header instead of incorrect license header
        return text.contains("Apache License")
    }

    private val copyrightHeader: String
        get() =
            """
            /*
             * Copyright (C) ${Year.now().value} 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.
             */
            """
                .trimIndent()
                .trim()

    companion object {
        @JvmField
        val ISSUE: Issue =
            Issue.create(
                id = "MissingApacheLicenseDetector",
                briefDescription = "File is missing Apache license information",
                explanation =
                    """
                        Every source code file should have copyright and license information \
                        attached at the beginning.""",
                category = Category.COMPLIANCE,
                priority = 8,
                // ignored by default and then explicitly overridden in SysUI's soong configuration
                severity = Severity.IGNORE,
                implementation =
                    Implementation(MissingApacheLicenseDetector::class.java, Scope.JAVA_FILE_SCOPE),
            )
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ class SystemUIIssueRegistry : IssueRegistry() {
                StaticSettingsProviderDetector.ISSUE,
                DemotingTestWithoutBugDetector.ISSUE,
                TestFunctionNameViolationDetector.ISSUE,
                MissingApacheLicenseDetector.ISSUE,
            )

    override val api: Int
+92 −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.internal.systemui.lint

import com.android.tools.lint.checks.infrastructure.TestMode
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import java.time.Year
import org.junit.Test

class MissingApacheLicenseDetectorTest : SystemUILintDetectorTest() {
    override fun getDetector(): Detector {
        return MissingApacheLicenseDetector()
    }

    override fun getIssues(): List<Issue> {
        return listOf(
            MissingApacheLicenseDetector.ISSUE,
        )
    }

    @Test
    fun testHasCopyright() {
        lint()
            .files(
                kotlin(
                    """
                    /*
                     * Copyright (C) ${Year.now().value} 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 test.pkg.name

                    class MyTest
                    """
                        .trimIndent()
                )
            )
            .issues(MissingApacheLicenseDetector.ISSUE)
            .run()
            .expectClean()
    }

    @Test
    fun testDoesntHaveCopyright() {
        lint()
            .files(
                kotlin(
                    """
                    package test.pkg.name

                    class MyTest
                    """
                        .trimIndent()
                )
            )
            // skipping mode SUPPRESSIBLE because lint tries to add @Suppress to class which
            // probably doesn't make much sense for license header (which is far above it) and for
            // kotlin files that can have several classes. If someone really wants to omit header
            // they can do it with //noinspection
            .skipTestModes(TestMode.SUPPRESSIBLE)
            .issues(MissingApacheLicenseDetector.ISSUE)
            .run()
            .expectContains("License header is missing")
    }
}