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

Commit d552d1fb authored by Michal Brzezinski's avatar Michal Brzezinski Committed by Michał Brzeziński
Browse files

Adding linter for checking license (and copyright) header in java/kotlin files

Fixes: 302679200
Test: MissingApacheLicenseDetectorTest
Test: run `m SYSTEM_UI_CORE_PATH/lint-report.html` and see legit license warnings/errors
Change-Id: I8c79e74f6a171964b30b93e029eeef0ef72a045c
parent 6d617f78
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -212,6 +212,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")
    }
}