Add License rule (#2429)

* #1515. Add rule to check license header.

* TODO. Tests.

* Add location to `Config`.

* Set Config location.

* Implement license header caching based on a FileProcessListener

* Introduce new SetupContext api which is able to provide rule authors with processing settings like the configuration path

* Revert yaml config change

Co-authored-by: Denys M <dector9@gmail.com>
This commit is contained in:
Artur Bosch
2020-03-15 09:05:20 +01:00
committed by GitHub
parent 8191b953ad
commit 74e86ccaa0
25 changed files with 319 additions and 16 deletions

View File

@@ -184,7 +184,8 @@ subprojects {
kotlinOptions.freeCompilerArgs = listOf(
"-progressive",
"-Xskip-runtime-version-check",
"-Xdisable-default-scripting-plugin"
"-Xdisable-default-scripting-plugin",
"-Xopt-in=kotlin.RequiresOptIn"
)
kotlinOptions.allWarningsAsErrors = shouldTreatCompilerWarningsAsErrors()
}

View File

@@ -6,12 +6,14 @@ package io.gitlab.arturbosch.detekt.api
* - [FileProcessListener]
* - [ConsoleReport]
* - [OutputReport]
* - [ConfigValidator]
*/
interface Extension {
/**
* Name of the extension.
*/
val id: String get() = javaClass.simpleName
/**
* Is used to run extensions in a specific order.
* The higher the priority the sooner the extension will run in detekt's lifecycle.
@@ -23,6 +25,14 @@ interface Extension {
* to setup this extension.
*/
fun init(config: Config) {
// for setup code
// implement for setup code
}
/**
* Setup extension by querying common paths and config options.
*/
@OptIn(UnstableApi::class)
fun init(context: SetupContext) {
// implement for setup code
}
}

View File

@@ -0,0 +1,19 @@
package io.gitlab.arturbosch.detekt.api
import java.net.URI
/**
* Context providing useful processing settings to initialize extensions.
*/
@UnstableApi
interface SetupContext {
/**
* All config locations which where used to create [config].
*/
val configUris: Collection<URI>
/**
* Configuration which is used to setup detekt.
*/
val config: Config
}

View File

@@ -0,0 +1,7 @@
package io.gitlab.arturbosch.detekt.api
/**
* Experimental detekt api which may change on minor or patch versions.
*/
@RequiresOptIn
annotation class UnstableApi

View File

@@ -6,6 +6,8 @@ import io.gitlab.arturbosch.detekt.api.internal.DisabledAutoCorrectConfig
import io.gitlab.arturbosch.detekt.api.internal.FailFastConfig
import io.gitlab.arturbosch.detekt.api.internal.PathFilters
import io.gitlab.arturbosch.detekt.api.internal.YamlConfig
import java.net.URI
import java.net.URL
import java.nio.file.Path
fun CliArgs.createFilters(): PathFilters? = PathFilters.of(includes, excludes)
@@ -71,3 +73,10 @@ private fun parsePathConfig(configPath: String): Config {
const val DEFAULT_CONFIG = "default-detekt-config.yml"
fun loadDefaultConfig() = YamlConfig.loadResource(ClasspathResourceConverter().convert(DEFAULT_CONFIG))
fun CliArgs.extractUris(): Collection<URI> {
val pathUris = config?.let { MultipleExistingPathConverter().convert(it).map(Path::toUri) } ?: emptyList()
val resourceUris = configResource?.let { MultipleClasspathResourceConverter().convert(it).map(URL::toURI) }
?: emptyList()
return resourceUris + pathUris
}

View File

@@ -23,6 +23,7 @@ class OutputFacade(
reports.forEach { report ->
report.init(config)
report.init(settings)
when (report) {
is ConsoleReport -> handleConsoleReport(report, result)
is OutputReport -> handleOutputReport(report, result)

View File

@@ -7,7 +7,9 @@ import io.gitlab.arturbosch.detekt.core.ProcessingSettings
import java.util.ServiceLoader
fun loadValidators(settings: ProcessingSettings): List<ConfigValidator> =
ServiceLoader.load(ConfigValidator::class.java, settings.pluginLoader).toList()
ServiceLoader.load(ConfigValidator::class.java, settings.pluginLoader)
.onEach { it.init(settings.config); it.init(settings) }
.toList()
fun checkConfiguration(settings: ProcessingSettings) {
val props = settings.config.subConfig("config")

View File

@@ -11,6 +11,7 @@ import io.gitlab.arturbosch.detekt.cli.console.red
import io.gitlab.arturbosch.detekt.cli.createClasspath
import io.gitlab.arturbosch.detekt.cli.createFilters
import io.gitlab.arturbosch.detekt.cli.createPlugins
import io.gitlab.arturbosch.detekt.cli.extractUris
import io.gitlab.arturbosch.detekt.cli.getOrComputeWeightedAmountOfIssues
import io.gitlab.arturbosch.detekt.cli.isValidAndSmallerOrEqual
import io.gitlab.arturbosch.detekt.cli.loadConfiguration
@@ -89,8 +90,8 @@ class Runner(
languageVersion = languageVersion,
jvmTarget = jvmTarget,
debug = arguments.debug,
outPrinter = outputPrinter,
errorPrinter = errorPrinter
outPrinter = outputPrinter, errorPrinter = errorPrinter,
configUris = extractUris()
)
}
settings.debug { "Loading config took $configLoadTime ms" }

View File

@@ -9,3 +9,4 @@ io.gitlab.arturbosch.detekt.core.processors.ProjectLLOCProcessor
io.gitlab.arturbosch.detekt.core.processors.ProjectCLOCProcessor
io.gitlab.arturbosch.detekt.core.processors.ProjectLOCProcessor
io.gitlab.arturbosch.detekt.core.processors.ProjectSLOCProcessor
io.gitlab.arturbosch.detekt.rules.documentation.LicenceHeaderLoaderExtension

View File

@@ -34,13 +34,16 @@ console-reports:
comments:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
UndocumentedPublicClass:
active: false
searchInNestedClass: true

View File

@@ -15,8 +15,9 @@ class FileProcessorLocator(private val settings: ProcessingSettings) {
if (processorsActive) {
ServiceLoader.load(FileProcessListener::class.java, settings.pluginLoader)
.filter { it.id !in excludes }
.onEach { it.init(config) }
.onEach { it.init(config); it.init(settings) }
.toList()
.also { settings.debug { "Registered file processors: $it" } }
} else {
emptyList()
}

View File

@@ -1,6 +1,8 @@
package io.gitlab.arturbosch.detekt.core
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.SetupContext
import io.gitlab.arturbosch.detekt.api.UnstableApi
import io.gitlab.arturbosch.detekt.api.internal.PathFilters
import io.gitlab.arturbosch.detekt.api.internal.createCompilerConfiguration
import io.gitlab.arturbosch.detekt.api.internal.createKotlinCoreEnvironment
@@ -12,6 +14,7 @@ import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.utils.closeQuietly
import java.io.Closeable
import java.io.PrintStream
import java.net.URI
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
@@ -24,10 +27,11 @@ import java.util.concurrent.ForkJoinPool
* Always close the settings as dispose the Kotlin compiler and detekt class loader.
* If using a custom executor service be aware that detekt won't shut it down after use!
*/
@OptIn(UnstableApi::class)
@Suppress("LongParameterList")
class ProcessingSettings @JvmOverloads constructor(
val inputPaths: List<Path>,
val config: Config = Config.empty,
override val config: Config = Config.empty,
val pathFilters: PathFilters? = null,
val parallelCompilation: Boolean = false,
val excludeDefaultRuleSets: Boolean = false,
@@ -39,8 +43,9 @@ class ProcessingSettings @JvmOverloads constructor(
val outPrinter: PrintStream = System.out,
val errorPrinter: PrintStream = System.err,
val autoCorrect: Boolean = false,
val debug: Boolean = false
) : AutoCloseable, Closeable {
val debug: Boolean = false,
override val configUris: Collection<URI> = emptyList()
) : AutoCloseable, Closeable, SetupContext {
/**
* Single project input path constructor.
*/
@@ -58,7 +63,8 @@ class ProcessingSettings @JvmOverloads constructor(
outPrinter: PrintStream = System.out,
errorPrinter: PrintStream = System.err,
autoCorrect: Boolean = false,
debug: Boolean = false
debug: Boolean = false,
configUris: Collection<URI> = emptyList()
) : this(
listOf(inputPath),
config,
@@ -73,7 +79,8 @@ class ProcessingSettings @JvmOverloads constructor(
outPrinter,
errorPrinter,
autoCorrect,
debug
debug,
configUris
)
init {

View File

@@ -0,0 +1,47 @@
package io.gitlab.arturbosch.detekt.rules.documentation
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtFile
/**
* This rule will report every Kotlin source file which doesn't have required license header.
*
* @configuration licenseTemplateFile - path to file with license header template resolved relatively to config file
* (default: `'license.template'`)
*/
class AbsentOrWrongFileLicense(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
id = RULE_NAME,
severity = Severity.Maintainability,
description = "License text is absent or incorrect in the file.",
debt = Debt.FIVE_MINS
)
override fun visitCondition(root: KtFile): Boolean =
super.visitCondition(root) && root.hasLicenseHeader()
override fun visitKtFile(file: KtFile) {
if (!file.hasValidLicense()) {
report(CodeSmell(
issue,
Entity.from(file),
"Expected license not found or incorrect in the file: ${file.name}."
))
}
}
private fun KtFile.hasValidLicense(): Boolean = text.startsWith(getLicenseHeader())
companion object {
const val PARAM_LICENSE_TEMPLATE_FILE = "licenseTemplateFile"
const val DEFAULT_LICENSE_TEMPLATE_FILE = "license.template"
val RULE_NAME: String = AbsentOrWrongFileLicense::class.java.simpleName
}
}

View File

@@ -30,7 +30,7 @@ class KDocStyle(config: Config = Config.empty) : MultiRule() {
* It should end with proper punctuation or with a correct URL.
*
* @configuration endOfSentenceFormat - regular expression which should match the end of the first sentence in the KDoc
* (default: `([.?!][ \t\n\r\f<])|([.?!:]$)`)
* (default: `'([.?!][ \t\n\r\f<])|([.?!:]$)'`)
*/
@Suppress("MemberNameEqualsClassName")
class EndOfSentenceFormat(config: Config = Config.empty) : Rule(config) {

View File

@@ -0,0 +1,78 @@
package io.gitlab.arturbosch.detekt.rules.documentation
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.FileProcessListener
import io.gitlab.arturbosch.detekt.api.SetupContext
import io.gitlab.arturbosch.detekt.api.SingleAssign
import io.gitlab.arturbosch.detekt.api.UnstableApi
import io.gitlab.arturbosch.detekt.rules.documentation.AbsentOrWrongFileLicense.Companion.DEFAULT_LICENSE_TEMPLATE_FILE
import io.gitlab.arturbosch.detekt.rules.documentation.AbsentOrWrongFileLicense.Companion.PARAM_LICENSE_TEMPLATE_FILE
import io.gitlab.arturbosch.detekt.rules.documentation.AbsentOrWrongFileLicense.Companion.RULE_NAME
import org.jetbrains.kotlin.com.intellij.openapi.util.Key
import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtilRt
import org.jetbrains.kotlin.psi.KtFile
import java.io.BufferedReader
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
@OptIn(UnstableApi::class)
class LicenceHeaderLoaderExtension : FileProcessListener {
private var config: Config by SingleAssign()
private var configPath: Path? = null
override fun init(context: SetupContext) {
this.config = context.config
this.configPath = context.configUris.lastOrNull()?.let(Paths::get)
}
override fun onStart(files: List<KtFile>) {
fun Config.isActive() = this.valueOrDefault(Config.ACTIVE_KEY, false)
fun shouldRuleRun(): Boolean {
val comments = config.subConfig("comments")
val ruleConfig = comments.subConfig(RULE_NAME)
return comments.isActive() && ruleConfig.isActive()
}
fun getPathToTemplate(): String = config.subConfig("comments")
.subConfig(RULE_NAME)
.valueOrDefault(PARAM_LICENSE_TEMPLATE_FILE, DEFAULT_LICENSE_TEMPLATE_FILE)
fun loadLicence(dir: Path): String {
val templateFile = dir.resolve(getPathToTemplate())
require(Files.exists(templateFile)) {
"""
Rule '$RULE_NAME': License template file not found at `${templateFile.toAbsolutePath()}`.
Create file license header file or check your running path.
""".trimIndent()
}
return Files.newBufferedReader(templateFile)
.use(BufferedReader::readText)
.let(StringUtilRt::convertLineSeparators)
}
fun cacheLicence(dir: Path) {
val licenceHeader = loadLicence(dir)
for (file in files) {
file.putUserData(LICENCE_KEY, licenceHeader)
}
}
if (configPath != null && shouldRuleRun()) {
val configDir = configPath?.parent
if (configDir != null) {
cacheLicence(configDir)
}
}
}
}
internal val LICENCE_KEY = Key.create<String>("LICENCE_HEADER")
internal fun KtFile.hasLicenseHeader(): Boolean = this.getUserData(LICENCE_KEY) != null
internal fun KtFile.getLicenseHeader(): String = this.getUserData(LICENCE_KEY) ?: error("License header expected")

View File

@@ -3,6 +3,7 @@ package io.gitlab.arturbosch.detekt.rules.providers
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.internal.DefaultRuleSetProvider
import io.gitlab.arturbosch.detekt.rules.documentation.AbsentOrWrongFileLicense
import io.gitlab.arturbosch.detekt.rules.documentation.CommentOverPrivateFunction
import io.gitlab.arturbosch.detekt.rules.documentation.CommentOverPrivateProperty
import io.gitlab.arturbosch.detekt.rules.documentation.KDocStyle
@@ -27,7 +28,8 @@ class CommentSmellProvider : DefaultRuleSetProvider {
KDocStyle(config),
UndocumentedPublicClass(config),
UndocumentedPublicFunction(config),
UndocumentedPublicProperty(config)
UndocumentedPublicProperty(config),
AbsentOrWrongFileLicense(config)
))
}
}

View File

@@ -0,0 +1,69 @@
package io.gitlab.arturbosch.detekt.rules.documentation
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Finding
import io.gitlab.arturbosch.detekt.api.SetupContext
import io.gitlab.arturbosch.detekt.api.UnstableApi
import io.gitlab.arturbosch.detekt.api.internal.YamlConfig
import io.gitlab.arturbosch.detekt.test.assertThat
import io.gitlab.arturbosch.detekt.test.compileContentForTest
import io.gitlab.arturbosch.detekt.test.lint
import io.gitlab.arturbosch.detekt.test.resource
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.net.URI
import java.nio.file.Paths
internal class AbsentOrWrongFileLicenseSpec : Spek({
describe("AbsentOrWrongFileLicense rule") {
context("file with correct license header") {
it("reports nothing") {
val findings = checkLicence("""
/* LICENSE */
package cases
""".trimIndent())
assertThat(findings).isEmpty()
}
}
context("file with incorrect license header") {
it("reports missed license header") {
val findings = checkLicence("""
/* WRONG LICENSE */
package cases
""".trimIndent())
assertThat(findings).hasSize(1)
}
}
context("file with absent license header") {
it("reports missed license header") {
val findings = checkLicence("""
package cases
""".trimIndent())
assertThat(findings).hasSize(1)
}
}
}
})
@OptIn(UnstableApi::class)
private fun checkLicence(content: String): List<Finding> {
val file = compileContentForTest(content)
val resource = resource("license-config.yml")
val config = YamlConfig.load(Paths.get(resource))
LicenceHeaderLoaderExtension().apply {
init(object : SetupContext {
override val configUris: Collection<URI> = listOf(resource)
override val config: Config = config
})
onStart(listOf(file))
}
return AbsentOrWrongFileLicense().lint(file)
}

View File

@@ -0,0 +1,5 @@
comments:
active: true
AbsentOrWrongFileLicense:
active: true
licenseTemplateFile: "license.template"

View File

@@ -0,0 +1 @@
/* LICENSE */

View File

@@ -9,6 +9,20 @@ folder: documentation
This rule set provides rules that address issues in comments and documentation
of the code.
### AbsentOrWrongFileLicense
This rule will report every Kotlin source file which doesn't have required license header.
**Severity**: Maintainability
**Debt**: 5min
#### Configuration options:
* ``licenseTemplateFile`` (default: ``'license.template'``)
path to file with license header template resolved relatively to config file
### CommentOverPrivateFunction
This rule reports comments and documentation that has been added to private functions. These comments get reported
@@ -50,7 +64,7 @@ It should end with proper punctuation or with a correct URL.
#### Configuration options:
* ``endOfSentenceFormat`` (default: ``([.?!][ \t\n\r\f<])|([.?!:]$)``)
* ``endOfSentenceFormat`` (default: ``'([.?!][ \t\n\r\f<])|([.?!:]$)'``)
regular expression which should match the end of the first sentence in the KDoc

View File

@@ -357,6 +357,13 @@ A rule set is a collection of rules and must be defined within a rule set provid
A rule set provider, as the name states, is responsible for creating rule sets.
|
##### [io.gitlab.arturbosch.detekt.api.SetupContext](../io.gitlab.arturbosch.detekt.api/-setup-context/index.html)
Context providing useful processing settings to initialize extensions.
|
##### [io.gitlab.arturbosch.detekt.api.Severity](../io.gitlab.arturbosch.detekt.api/-severity/index.html)
@@ -422,6 +429,13 @@ Provides a threshold attribute for this rule, which is specified manually for de
but can be also obtained from within a configuration object.
|
##### [io.gitlab.arturbosch.detekt.api.UnstableApi](../io.gitlab.arturbosch.detekt.api/-unstable-api/index.html)
Experimental detekt api which may change on minor or patch versions.
|
##### [io.gitlab.arturbosch.detekt.api.internal.ValidatableConfiguration](../io.gitlab.arturbosch.detekt.api.internal/-validatable-configuration/index.html)

View File

@@ -14,6 +14,7 @@ Currently supported extensions are:
* [FileProcessListener](../-file-process-listener/index.html)
* [ConsoleReport](../-console-report/index.html)
* [OutputReport](../-output-report/index.html)
* [ConfigValidator](../-config-validator/index.html)
### Properties
@@ -22,7 +23,7 @@ Currently supported extensions are:
### Functions
| [init](init.html) | Allows to read any or even user defined properties from the detekt yaml config to setup this extension.`open fun init(config: `[`Config`](../-config/index.html)`): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html) |
| [init](init.html) | Allows to read any or even user defined properties from the detekt yaml config to setup this extension.`open fun init(config: `[`Config`](../-config/index.html)`): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)<br>Setup extension by querying common paths and config options.`open fun init(context: `[`SetupContext`](../-setup-context/index.html)`): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html) |
### Inheritors

View File

@@ -11,3 +11,7 @@ title: Extension.init - detekt-api
Allows to read any or even user defined properties from the detekt yaml config
to setup this extension.
`open fun init(context: `[`SetupContext`](../-setup-context/index.html)`): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)
Setup extension by querying common paths and config options.

View File

@@ -13,6 +13,7 @@ yaml specification.
### Properties
| [location](location.html) | `val location: Location` |
| [parent](parent.html) | Returns the parent config which encloses this config part.`val parent: Parent?` |
| [properties](properties.html) | `val properties: `[`Map`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)`<`[`String`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)`, `[`Any`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)`>` |

View File

@@ -44,6 +44,7 @@ title: io.gitlab.arturbosch.detekt.api - detekt-api
| [RuleSet](-rule-set/index.html) | A rule set is a collection of rules and must be defined within a rule set provider implementation.`class RuleSet` |
| [RuleSetId](-rule-set-id.html) | `typealias RuleSetId = `[`String`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) |
| [RuleSetProvider](-rule-set-provider/index.html) | A rule set provider, as the name states, is responsible for creating rule sets.`interface RuleSetProvider` |
| [SetupContext](-setup-context/index.html) | Context providing useful processing settings to initialize extensions.`interface SetupContext` |
| [Severity](-severity/index.html) | Rules can classified into different severity grades. Maintainer can choose a grade which is most harmful to their projects.`enum class Severity` |
| [SingleAssign](-single-assign/index.html) | Allows to assign a property just once. Further assignments result in [IllegalStateException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-state-exception/index.html)'s.`class SingleAssign<T : `[`Any`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)`>` |
| [SourceLocation](-source-location/index.html) | Stores line and column information of a location.`data class SourceLocation` |
@@ -53,6 +54,10 @@ title: io.gitlab.arturbosch.detekt.api - detekt-api
| [ThresholdRule](-threshold-rule/index.html) | Provides a threshold attribute for this rule, which is specified manually for default values but can be also obtained from within a configuration object.`abstract class ThresholdRule : `[`Rule`](-rule/index.html) |
| [YamlConfig](-yaml-config.html) | Config implementation using the yaml format. SubConfigurations can return sub maps according to the yaml specification.`typealias ~~YamlConfig~~ = `[`YamlConfig`](../io.gitlab.arturbosch.detekt.api.internal/-yaml-config/index.html) |
### Annotations
| [UnstableApi](-unstable-api/index.html) | Experimental detekt api which may change on minor or patch versions.`annotation class UnstableApi` |
### Extensions for External Classes
| [kotlin.String](kotlin.-string/index.html) | |