From 6d699105add27e39dd7e5f09b482b82de8fbd7f1 Mon Sep 17 00:00:00 2001 From: "Juan C. Goncalves" Date: Fri, 16 Oct 2020 20:41:08 -0400 Subject: [PATCH] For #8947 - Lint check for licenses in Kotlin/Java source files (#15639) --- mozilla-lint-rules/build.gradle | 3 + .../fenix/lintrules/LicenseCommentChecker.kt | 84 +++++++++++ .../fenix/lintrules/LicenseDetector.kt | 46 ++++++ .../fenix/lintrules/LintIssueRegistry.kt | 4 +- .../fenix/lintrules/LicenseDetectorTest.kt | 140 ++++++++++++++++++ 5 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseCommentChecker.kt create mode 100644 mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseDetector.kt create mode 100644 mozilla-lint-rules/src/test/java/org/mozilla/fenix/lintrules/LicenseDetectorTest.kt diff --git a/mozilla-lint-rules/build.gradle b/mozilla-lint-rules/build.gradle index 57725a5c1..c57580af7 100644 --- a/mozilla-lint-rules/build.gradle +++ b/mozilla-lint-rules/build.gradle @@ -12,6 +12,9 @@ dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" compileOnly "com.android.tools.lint:lint-api:${Versions.android_lint_api}" compileOnly "com.android.tools.lint:lint-checks:${Versions.android_lint_api}" + + testImplementation "com.android.tools.lint:lint:${Versions.android_lint_api}" + testImplementation "com.android.tools.lint:lint-tests:${Versions.android_lint_api}" } jar { diff --git a/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseCommentChecker.kt b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseCommentChecker.kt new file mode 100644 index 000000000..b3511396b --- /dev/null +++ b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseCommentChecker.kt @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.lintrules + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.psi.psiUtil.startsWithComment +import org.jetbrains.uast.UComment +import org.jetbrains.uast.UFile + +class LicenseCommentChecker(private val context: JavaContext) : UElementHandler() { + + companion object { + val ValidLicenseForKotlinFiles = """ + |/* This Source Code Form is subject to the terms of the Mozilla Public + | * License, v. 2.0. If a copy of the MPL was not distributed with this + | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""".trimMargin() + } + + override fun visitFile(node: UFile) { + if (!node.sourcePsi.startsWithComment()) { + reportMissingLicense(node) + } else { + val firstComment = node.allCommentsInFile.first() + if (firstComment.text != ValidLicenseForKotlinFiles) { + reportInvalidLicenseFormat(firstComment) + } else { + val nextSibling = + firstComment.sourcePsi.siblings(withItself = false).firstOrNull() + if (nextSibling?.text != "\n\n") { + reportMissingLeadingNewLineCharacter(firstComment) + } + } + } + } + + private fun reportMissingLicense(node: UFile) = context.report( + LicenseDetector.ISSUE_MISSING_LICENSE, + context.getLocation(node.sourcePsi.firstChild), + "The file must start with a comment containing the license", + addLicenseQuickFix() + ) + + private fun reportInvalidLicenseFormat(comment: UComment) = context.report( + LicenseDetector.ISSUE_INVALID_LICENSE_FORMAT, + context.getLocation(comment), + "The license comment doesn't have the appropriate format", + replaceCommentWithValidLicenseFix(comment) + ) + + private fun reportMissingLeadingNewLineCharacter(licenseComment: UComment) = context.report( + LicenseDetector.ISSUE_INVALID_LICENSE_FORMAT, + context.getRangeLocation(licenseComment, licenseComment.text.lastIndex, 1), + "The license comment must be followed by a newline character", + addLeadingNewLineQuickFix(licenseComment) + ) + + private fun addLicenseQuickFix() = LintFix.create() + .name("Insert license at the top of the file") + .replace() + .beginning() + .with(ValidLicenseForKotlinFiles + "\n\n") + .autoFix() + .build() + + private fun replaceCommentWithValidLicenseFix(comment: UComment) = LintFix.create() + .name("Replace with correctly formatted license") + .replace() + .range(context.getLocation(comment)) + .with(ValidLicenseForKotlinFiles) + .build() + + private fun addLeadingNewLineQuickFix(licenseComment: UComment) = LintFix.create() + .name("Insert newline character after the license") + .replace() + .range(context.getLocation(licenseComment)) + .with(ValidLicenseForKotlinFiles + "\n") + .autoFix() + .build() +} \ No newline at end of file diff --git a/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseDetector.kt b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseDetector.kt new file mode 100644 index 000000000..1b6a7cfe3 --- /dev/null +++ b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LicenseDetector.kt @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.lintrules + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.* +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UFile + +class LicenseDetector : Detector(), SourceCodeScanner { + + companion object { + + private val Implementation = Implementation( + LicenseDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + + val ISSUE_MISSING_LICENSE = Issue.create( + id = "MissingLicense", + briefDescription = "File doesn't start with the license comment", + explanation = "Every file must start with the license comment:\n" + + LicenseCommentChecker.ValidLicenseForKotlinFiles, + category = Category.CORRECTNESS, + severity = Severity.WARNING, + implementation = Implementation + ) + + val ISSUE_INVALID_LICENSE_FORMAT = Issue.create( + id = "InvalidLicenseFormat", + briefDescription = "License isn't formatted correctly", + explanation = "The license must be:\n${LicenseCommentChecker.ValidLicenseForKotlinFiles}", + category = Category.CORRECTNESS, + severity = Severity.WARNING, + implementation = Implementation + ) + } + + override fun getApplicableUastTypes(): List>? = listOf(UFile::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler? = + LicenseCommentChecker(context) + +} diff --git a/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LintIssueRegistry.kt b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LintIssueRegistry.kt index 7f3945eb9..fa2ed2f65 100644 --- a/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LintIssueRegistry.kt +++ b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/LintIssueRegistry.kt @@ -17,6 +17,8 @@ class LintIssueRegistry : IssueRegistry() { ButtonStyleXmlDetector.ISSUE_XML_STYLE, AndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE, TextViewAndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE, - ImageViewAndroidTintXmlDetector.ISSUE_XML_SRC_USAGE + ImageViewAndroidTintXmlDetector.ISSUE_XML_SRC_USAGE, + LicenseDetector.ISSUE_MISSING_LICENSE, + LicenseDetector.ISSUE_INVALID_LICENSE_FORMAT ) + ConstraintLayoutPerfDetector.ISSUES } diff --git a/mozilla-lint-rules/src/test/java/org/mozilla/fenix/lintrules/LicenseDetectorTest.kt b/mozilla-lint-rules/src/test/java/org/mozilla/fenix/lintrules/LicenseDetectorTest.kt new file mode 100644 index 000000000..86c915a5c --- /dev/null +++ b/mozilla-lint-rules/src/test/java/org/mozilla/fenix/lintrules/LicenseDetectorTest.kt @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.lintrules + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test +import org.mozilla.fenix.lintrules.LicenseCommentChecker.Companion.ValidLicenseForKotlinFiles +import org.mozilla.fenix.lintrules.LicenseDetector.Companion.ISSUE_INVALID_LICENSE_FORMAT +import org.mozilla.fenix.lintrules.LicenseDetector.Companion.ISSUE_MISSING_LICENSE + +class LicenseDetectorTest { + + @Test + fun `report missing license if it isn't at the top of the file`() { + val code = """ + |package example + | + |class SomeExample { + | fun test() { + | val x = 10 + | println("Hello") + | } + |}""".trimMargin() + + val expectedReport = """ + |src/example/SomeExample.kt:1: Warning: The file must start with a comment containing the license [MissingLicense] + |package example + |~~~~~~~~~~~~~~~ + |0 errors, 1 warnings""".trimMargin() + + val expectedFixOutput = """ + |Fix for src/example/SomeExample.kt line 1: Insert license at the top of the file: + |@@ -1 +1 + |+ /* This Source Code Form is subject to the terms of the Mozilla Public + |+ * License, v. 2.0. If a copy of the MPL was not distributed with this + |+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + |+""".trimMargin() + + lint() + .files(TestFiles.kt(code)) + .issues(ISSUE_MISSING_LICENSE, ISSUE_INVALID_LICENSE_FORMAT) + .run() + .expect(expectedReport) + .expectFixDiffs(expectedFixOutput) + } + + @Test + fun `report invalid license format if it doesn't have a leading newline character`() { + val code = """ + |$ValidLicenseForKotlinFiles + |package example + | + |class SomeExample { + | fun test() { + | val x = 10 + | println("Hello") + | } + |}""".trimMargin() + + val expectedReport = """ + |src/example/SomeExample.kt:3: Warning: The license comment must be followed by a newline character [InvalidLicenseFormat] + | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + | ~ + |0 errors, 1 warnings""".trimMargin() + + val expectedFixOutput = """ + |Fix for src/example/SomeExample.kt line 3: Insert newline character after the license: + |@@ -4 +4 + |+""".trimMargin() + + lint() + .files(TestFiles.kt(code)) + .issues(ISSUE_MISSING_LICENSE, ISSUE_INVALID_LICENSE_FORMAT) + .run() + .expect(expectedReport) + .expectFixDiffs(expectedFixOutput) + } + + @Test + fun `valid license does not generate a warning`() { + val code = """ + |$ValidLicenseForKotlinFiles + | + |package example + | + |class SomeExample { + | fun test() { + | val x = 10 + | println("Hello") + | } + |}""".trimMargin() + + lint() + .files(TestFiles.kt(code)) + .issues(ISSUE_MISSING_LICENSE, ISSUE_INVALID_LICENSE_FORMAT) + .run() + .expectClean() + } + + @Test + fun `report incorrectly formatted license`() { + val code = """ + |/* This Source Code Form is subject to the terms of the Mozilla Public + | License, v. 2.0. If a copy of the MPL was not distributed with this + | file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + | + |package example + | + |class SomeExample { + | fun test() { + | val x = 10 + | println("Hello") + | } + |}""".trimMargin() + + val expectedReport = """ + |src/example/SomeExample.kt:1: Warning: The license comment doesn't have the appropriate format [InvalidLicenseFormat] + |/* This Source Code Form is subject to the terms of the Mozilla Public + |^ + |0 errors, 1 warnings""".trimMargin() + + val expectedFixOutput = """ + |Fix for src/example/SomeExample.kt line 1: Replace with correctly formatted license: + |@@ -2 +2 + |- License, v. 2.0. If a copy of the MPL was not distributed with this + |- file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + |+ * License, v. 2.0. If a copy of the MPL was not distributed with this + |+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""".trimMargin() + + lint() + .files(TestFiles.kt(code)) + .issues(ISSUE_MISSING_LICENSE, ISSUE_INVALID_LICENSE_FORMAT) + .run() + .expect(expectedReport) + .expectFixDiffs(expectedFixOutput) + } +} \ No newline at end of file