blob: 79740039c309d4a53503a7bf6cf14dbdcaa20990 [file] [log] [blame]
/*
* Copyright (C) 2024 Dremio
*
* 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 copiedcode
import java.nio.file.Files
import java.util.regex.Pattern
import javax.inject.Inject
import kotlin.collections.joinToString
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.component.SoftwareComponentFactory
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.work.DisableCachingByDefault
/**
* This plugin identifies files that have been originally copied from another project.
*
* Configuration is done using the [CopiedCodeCheckerExtension], available under the name
* `copiedCodeChecks`.
*
* Such files need to contain a magic word, see [CopiedCodeCheckerExtension.magicWord].
*
* This plugin scans all source directories configured in the project's [SourceDirectorySet]. Files
* in the project's build directory are always excluded.
*
* By default, this plugin scans all files. There is a convenience function to exclude known binary
* types, see [CopiedCodeCheckerExtension.addDefaultContentTypes]. The
* [CopiedCodeCheckerExtension.excludedContentTypePatterns] is checked first against a detected
* content type. If a content-type's excluded, the
* [CopiedCodeCheckerExtension.includedContentTypePatterns] is consulted. If a content-type could
* not be detected, the property [CopiedCodeCheckerExtension.includeUnrecognizedContentType], which
* defaults to `true`, is consulted.
*
* Each Gradle project has its own instance of the [CopiedCodeCheckerExtension], the extension of
* the root project serves default values, except for [CopiedCodeCheckerExtension.scanDirectories]]
*
* The license file to check is configured via [CopiedCodeCheckerExtension.licenseFile]. Files must
* be mentioned using the relative path from the root directory, with a trailing `* ` (star +
* space).
*/
@Suppress("unused")
class CopiedCodeCheckerPlugin
@Inject
constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Plugin<Project> {
override fun apply(project: Project): Unit =
project.run {
val extension =
extensions.create("copiedCodeChecks", CopiedCodeCheckerExtension::class.java, project)
if (rootProject == this) {
// Apply this plugin to all projects
afterEvaluate { subprojects { plugins.apply(CopiedCodeCheckerPlugin::class.java) } }
tasks.register(
CHECK_COPIED_CODE_MENTIONS_EXIST_TASK_NAME,
CheckCopiedCodeMentionsExistTask::class.java,
)
afterEvaluate {
tasks.named("check").configure { dependsOn(CHECK_COPIED_CODE_MENTIONS_EXIST_TASK_NAME) }
}
} else {
extension.excludedContentTypePatterns.convention(
provider {
rootProject.extensions
.getByType(CopiedCodeCheckerExtension::class.java)
.excludedContentTypePatterns
.get()
}
)
extension.includedContentTypePatterns.convention(
provider {
rootProject.extensions
.getByType(CopiedCodeCheckerExtension::class.java)
.includedContentTypePatterns
.get()
}
)
extension.includeUnrecognizedContentType.convention(
provider {
rootProject.extensions
.getByType(CopiedCodeCheckerExtension::class.java)
.includeUnrecognizedContentType
.get()
}
)
extension.licenseFile.convention(
provider {
rootProject.extensions
.getByType(CopiedCodeCheckerExtension::class.java)
.licenseFile
.get()
}
)
}
tasks.register(CHECK_FOR_COPIED_CODE_TASK_NAME, CheckForCopiedCodeTask::class.java)
afterEvaluate {
tasks.named("check").configure { dependsOn(CHECK_FOR_COPIED_CODE_TASK_NAME) }
}
}
companion object {
private const val CHECK_FOR_COPIED_CODE_TASK_NAME = "checkForCopiedCode"
private const val CHECK_COPIED_CODE_MENTIONS_EXIST_TASK_NAME = "checkCopiedCodeMentionsExist"
}
}
@DisableCachingByDefault
abstract class CheckCopiedCodeMentionsExistTask : DefaultTask() {
@TaskAction
fun checkMentions() {
val extension = project.extensions.getByType(CopiedCodeCheckerExtension::class.java)
val licenseFile = extension.licenseFile.get().asFile
val licenseFileRelative = licenseFile.relativeTo(project.rootDir).toString()
logger.info("Checking whether files mentioned in the {} file exist", licenseFileRelative)
val nonExistingMentions =
extension.licenseFile
.get()
.asFile
.readLines()
.filter { line -> line.startsWith("* ") && line.length > 2 }
.map { line -> line.substring(2) }
.filter { relFilePath -> !project.rootProject.file(relFilePath).exists() }
.sorted()
if (nonExistingMentions.isNotEmpty()) {
logger.error(
"""
The following {} files mentioned in {} do not exist, fix the {} file.
{}
"""
.trimIndent(),
nonExistingMentions.size,
licenseFileRelative,
licenseFileRelative,
nonExistingMentions.joinToString("\n* ", "* "),
)
throw GradleException(
"${nonExistingMentions.size} files mentioned in $licenseFileRelative do not exist, fix the $licenseFileRelative file."
)
}
}
}
@DisableCachingByDefault
abstract class CheckForCopiedCodeTask : DefaultTask() {
private fun namedDirectorySets(): List<Pair<String, SourceDirectorySet>> {
val namedDirectorySets = mutableListOf<Pair<String, SourceDirectorySet>>()
val extension = project.extensions.getByType(CopiedCodeCheckerExtension::class.java)
extension.scanDirectories.forEach { scanDirectory ->
namedDirectorySets.add(Pair("scan directory ${scanDirectory.name}", scanDirectory))
}
val sourceSets: SourceSetContainer? by project
sourceSets?.forEach { sourceSet ->
namedDirectorySets.add(Pair("source set ${sourceSet.name}", sourceSet.allSource))
}
return namedDirectorySets
}
@TaskAction
fun checkForCopiedCode() {
val extension = project.extensions.getByType(CopiedCodeCheckerExtension::class.java)
val licenseFile = extension.licenseFile.get().asFile
val licenseFileRelative = licenseFile.relativeTo(project.rootDir).toString()
logger.info("Running copied code check against root project's {} file", licenseFileRelative)
val namedDirectorySets = namedDirectorySets()
val includedPatterns = extension.includedContentTypePatterns.get().map { Pattern.compile(it) }
val excludedPatterns = extension.includedContentTypePatterns.get().map { Pattern.compile(it) }
val includeUnknown = extension.includeUnrecognizedContentType.get()
val magicWord = extension.magicWord.get()
val magicWordPattern = Pattern.compile(".*\\b${magicWord}\\b.*")
val mentionedFilesInLicense =
extension.licenseFile
.get()
.asFile
.readLines()
.filter { line -> line.startsWith("* ") && line.length > 2 }
.map { line -> line.substring(2) }
.toSet()
val buildDir = project.layout.buildDirectory.asFile.get()
val unmentionedFiles =
namedDirectorySets
.flatMap { pair ->
val name = pair.first
val sourceDirectorySet = pair.second
logger.info(
"Checking {} for files containing {} not mentioned in {}",
name,
magicWord,
licenseFileRelative,
)
sourceDirectorySet.asFileTree
.filter { file -> !file.startsWith(buildDir) }
.map { file ->
val projectRelativeFile = file.relativeTo(project.projectDir)
val fileType = Files.probeContentType(file.toPath())
logger.info(
"Checking file '{}' (probed content type: {})",
projectRelativeFile,
fileType,
)
var r: String? = null
var check = true
if (fileType == null) {
if (!includeUnknown) {
logger.info(" ... unknown content type, skipping")
check = false
}
} else {
val excluded =
excludedPatterns.any { pattern -> pattern.matcher(fileType).matches() }
if (excluded) {
val included =
includedPatterns.any { pattern -> pattern.matcher(fileType).matches() }
if (!included) {
logger.info(" ... excluded and not included content type, skipping")
check = false
}
}
}
if (check) {
if (!file.readLines().any { s -> magicWordPattern.matcher(s).matches() }) {
logger.info(
" ... no magic word, not expecting an entry in {}",
licenseFileRelative,
)
} else {
val relativeFilePath = file.relativeTo(project.rootProject.projectDir).toString()
if (mentionedFilesInLicense.contains(relativeFilePath)) {
logger.info(" ... has magic word & mentioned in {}", licenseFileRelative)
} else {
// error (summary) logged below
logger.info(
"The file '{}' has the {} marker, but is not mentioned in {}",
relativeFilePath,
magicWord,
licenseFileRelative,
)
r = relativeFilePath
}
}
}
r
}
.filter { r -> r != null }
.map { r -> r!! }
}
.sorted()
.toList()
if (!unmentionedFiles.isEmpty()) {
logger.error(
"""
The following {} files have the {} marker but are not mentioned in {}, add those in an appropriate section.
{}
"""
.trimIndent(),
unmentionedFiles.size,
magicWord,
licenseFileRelative,
unmentionedFiles.joinToString("\n* ", "* "),
)
throw GradleException(
"${unmentionedFiles.size} files with the $magicWord marker need to be mentioned in $licenseFileRelative. See the messages above."
)
}
}
}