Clearer warnings in license review (#9134)

- Closes #9120
- Reorders CI steps to do the license check last (to avoid it preventing tests from running which are more important than the license check)
- Tries to reword the warnings to be clearer
- Adds some CSS to the report to more clearly indicate which elements can be clicked.
This commit is contained in:
Radosław Waśko 2024-02-27 17:32:08 +01:00 committed by GitHub
parent edb349f8fc
commit 47c64167ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 370 additions and 251 deletions

View File

@ -349,12 +349,6 @@ impl RunContext {
tasks.push("engine-runner/buildNativeImage");
}
if TARGET_OS != OS::Windows {
// FIXME [mwu] apparently this is broken on Windows because of the line endings
// mismatch
tasks.push("verifyLicensePackages");
}
if self.config.build_project_manager_package() {
tasks.push("buildProjectManagerDistribution");
}
@ -565,8 +559,14 @@ impl RunContext {
runner_sanity_test(&self.repo_root, Some(enso_java)).await?;
}
// Verify the status of the License Review Report
if TARGET_OS != OS::Windows {
// FIXME [mwu] apparently this is broken on Windows because of the line endings
// mismatch
sbt.call_arg("verifyLicensePackages").await?;
}
// Verify License Packages in Distributions
// Verify Integrity of Generated License Packages in Distributions
// FIXME apparently this does not work on Windows due to some CRLF issues?
if self.config.verify_packages && TARGET_OS != OS::Windows {
for package in ret.packages() {

View File

@ -66,11 +66,6 @@ The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.guava.guava-32.0.0-jre`.
'listenablefuture', licensed under the The Apache Software License, Version 2.0, is distributed with the engine.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava`.
'j2objc-annotations', licensed under the Apache License, Version 2.0, is distributed with the engine.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.j2objc.j2objc-annotations-2.8`.

View File

@ -1,2 +0,0 @@
See com.google.guava.guava-29.0-jre for licensing information.

View File

@ -91,11 +91,6 @@ The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.guava.guava-32.1.3-jre`.
'listenablefuture', licensed under the The Apache Software License, Version 2.0, is distributed with the Google_Api.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava`.
'google-http-client', licensed under the The Apache Software License, Version 2.0, is distributed with the Google_Api.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.http-client.google-http-client-1.43.3`.

View File

@ -1,2 +0,0 @@
See com.google.guava.guava-32.1.3-jre for licensing information.

View File

@ -44,9 +44,8 @@ object GatherLicenses {
s"It consists of the following sbt project roots:" +
s" ${projectNames.mkString(", ")}"
)
val (sbtInfo, sbtWarnings) =
val (sbtInfo, sbtDiagnostics) =
SbtLicenses.analyze(distribution.sbtComponents, log)
sbtWarnings.foreach(log.warn(_))
val allInfo = sbtInfo // TODO [RW] add Rust frontend result here (#1187)
@ -59,32 +58,42 @@ object GatherLicenses {
s"${dependency.url}"
)
val defaultAttachments = defaultBackend.run(dependency.sources)
val attachments =
if (defaultAttachments.nonEmpty) defaultAttachments
val WithDiagnostics(attachments, attachmentDiagnostics) =
if (defaultAttachments.nonEmpty) WithDiagnostics(defaultAttachments)
else GithubHeuristic(dependency, log).run()
(dependency, attachments)
(dependency, attachments, attachmentDiagnostics)
}
val summary = DependencySummary(processed)
val distributionRoot = configRoot / distribution.artifactName
val WithWarnings(processedSummary, summaryWarnings) =
val forSummary = processed.map(t => (t._1, t._2))
val processingDiagnostics = processed.flatMap(_._3)
val summary = DependencySummary(forSummary)
val distributionRoot = configRoot / distribution.artifactName
val WithDiagnostics(processedSummary, summaryDiagnostics) =
Review(distributionRoot, summary).run()
val allWarnings = sbtWarnings ++ summaryWarnings
val allDiagnostics =
sbtDiagnostics ++ processingDiagnostics ++ summaryDiagnostics
val reportDestination =
targetRoot / s"${distribution.artifactName}-report.html"
sbtWarnings.foreach(log.warn(_))
if (summaryWarnings.size > 10)
log.warn(
s"There are too many warnings (${summaryWarnings.size}) to " +
s"display. Please inspect the generated report."
)
else allWarnings.foreach(log.warn(_))
val (warnings: Seq[Diagnostic.Warning], errors: Seq[Diagnostic.Error]) =
Diagnostic.partition(allDiagnostics)
if (warnings.nonEmpty) {
log.warn(s"Found ${warnings.size} non-fatal warnings in the report:")
warnings.foreach(notice => log.warn(notice.message))
}
if (errors.isEmpty) {
log.info("No fatal errors found in the report.")
} else {
log.error(s"Found ${errors.size} fatal errors in the report:")
errors.foreach(problem => log.error(problem.message))
}
Report.writeHTML(
distribution,
processedSummary,
allWarnings,
allDiagnostics,
reportDestination
)
log.info(
@ -96,10 +105,10 @@ object GatherLicenses {
ReportState.write(
distributionRoot / stateFileName,
distribution,
summaryWarnings.length
errors.size
)
log.info(s"Re-generated distribution notices at `$packagePath`.")
if (summaryWarnings.nonEmpty) {
if (errors.nonEmpty) {
log.warn(
"The distribution notices were regenerated, but there are " +
"not-reviewed issues within the report. The notices are probably " +
@ -176,7 +185,7 @@ object GatherLicenses {
.getOrElse(
throw LegalReviewException(
s"Report at $distributionConfig is not available. " +
s"Make sure to run `enso/gatherLicenses`."
s"Make sure to run `enso/gatherLicenses` or `openLegalReviewReport`."
)
)

View File

@ -2,9 +2,10 @@ package src.main.scala.licenses
import sbt.IO
import src.main.scala.licenses.report.{
Diagnostic,
LicenseReview,
PackageNotices,
WithWarnings
WithDiagnostics
}
/** Contains a sequence of dependencies and any attachments found.
@ -122,37 +123,54 @@ object ReviewedSummary {
/** Returns a list of warnings that indicate missing reviews or other issues.
*/
def warnAboutMissingReviews(summary: ReviewedSummary): WithWarnings[Unit] = {
val warnings = summary.dependencies.flatMap { dep =>
val warnings = collection.mutable.Buffer[String]()
val name = dep.information.moduleInfo.toString
def warnAboutMissingReviews(
summary: ReviewedSummary
): WithDiagnostics[Unit] = {
val diagnostics = summary.dependencies.flatMap { dep =>
val diagnostics = collection.mutable.Buffer[Diagnostic]()
val name = dep.information.moduleInfo.toString
val missingFiles = dep.files.filter(_._2 == AttachmentStatus.NotReviewed)
if (missingFiles.nonEmpty) {
warnings.append(
s"${missingFiles.size} files are not reviewed in $name."
diagnostics.append(
Diagnostic.Error(
s"${missingFiles.size} files are not reviewed in $name."
)
)
}
val missingCopyrights =
dep.copyrights.filter(_._2 == AttachmentStatus.NotReviewed)
if (missingCopyrights.nonEmpty) {
warnings.append(
s"${missingCopyrights.size} copyrights are not reviewed in $name."
diagnostics.append(
Diagnostic.Error(
s"${missingCopyrights.size} copyrights are not reviewed in $name."
)
)
}
val includedInfos =
(dep.files.map(_._2) ++ dep.copyrights.map(_._2)).filter(_.included)
if (includedInfos.isEmpty) {
warnings.append(
s"No files or copyright information are included for $name."
diagnostics.append(
Diagnostic.Error(
s"No files or copyright information are included for $name. " +
s"Generally every dependency should have _some_ copyright info, so " +
s"this suggests all our heuristics failed. " +
s"Please find the information manually and add it using `files-add` " +
s"or `copyright-add`. Even if the dependency is in public domain, " +
s"it may be good to include some information about its source."
)
)
}
dep.licenseReview match {
case LicenseReview.NotReviewed =>
warnings.append(
s"License ${dep.information.license.name} for $name is not reviewed."
diagnostics.append(
Diagnostic.Error(
s"Default license ${dep.information.license.name} for $name is " +
s"used, but that license is not reviewed " +
s"(need to add an entry to `reviewed-licenses`)."
)
)
case LicenseReview.Default(
defaultPath,
@ -163,10 +181,17 @@ object ReviewedSummary {
case Some(includedLicense) =>
val licenseContent = IO.read(defaultPath.toFile)
if (licenseContent.strip != includedLicense.content) {
warnings.append(
s"A license file was discovered in $name that is different " +
s"from the default license file that is associated with its " +
s"license ${dep.information.license.name}."
diagnostics.append(
Diagnostic.Error(
s"A license file was discovered in $name that is different " +
s"from the default license file that is associated with its " +
s"license ${dep.information.license.name}, " +
s"but a custom license was not expected. " +
s"If this custom license should override the default one, " +
s"create a `custom-license` config file. " +
s"If both files are expected to be included, " +
s"create an empty `default-and-custom-license` file."
)
)
}
case None =>
@ -178,14 +203,16 @@ object ReviewedSummary {
val fileWillBeIncludedAsCopyrightNotices =
filename == PackageNotices.gatheredNoticesFilename
if (!fileIsIncluded && !fileWillBeIncludedAsCopyrightNotices) {
warnings.append(
s"License for $name is set to custom file `$filename`, but no such file is attached."
diagnostics.append(
Diagnostic.Error(
s"License for $name is set to custom file `$filename`, but no such file is attached."
)
)
}
}
warnings
diagnostics
}
WithWarnings.justWarnings(warnings)
WithDiagnostics.justDiagnostics(diagnostics)
}
}

View File

@ -1,9 +1,9 @@
package src.main.scala.licenses.backend
import java.nio.file.Path
import sbt.Logger
import sbt.io.syntax.url
import src.main.scala.licenses.report.{Diagnostic, WithDiagnostics}
import src.main.scala.licenses.{
AttachedFile,
Attachment,
@ -11,7 +11,7 @@ import src.main.scala.licenses.{
PortablePath
}
import scala.sys.process._
import scala.sys.process.*
import scala.util.control.NonFatal
/** Tries to find copyright mentions in the GitHub project homepage and any
@ -28,11 +28,11 @@ case class GithubHeuristic(info: DependencyInformation, log: Logger) {
*
* It proceeds only if the project has an URL that seems to point to GitHub.
*/
def run(): Seq[Attachment] = {
def run(): WithDiagnostics[Seq[Attachment]] = {
info.url match {
case Some(url) if url.contains("github.com") =>
tryDownloadingAttachments(url.replace("http://", "https://"))
case _ => Seq()
case _ => WithDiagnostics(Seq(), Seq())
}
}
@ -41,15 +41,23 @@ case class GithubHeuristic(info: DependencyInformation, log: Logger) {
*
* Any found files are fetched and saved into the results.
*/
def tryDownloadingAttachments(address: String): Seq[Attachment] =
def tryDownloadingAttachments(
address: String
): WithDiagnostics[Seq[Attachment]] =
try {
val homePage = url(address).cat.!!
val branchRegex = """"defaultBranch":"([^"]*?)"""".r("branch")
val branch = branchRegex.findFirstMatchIn(homePage).map(_.group("branch"))
branch match {
case None =>
log.warn(s"Cannot find default branch for $address")
Seq()
WithDiagnostics(
Seq(),
Seq(
Diagnostic.Error(
s"GitHub heuristic failure: Cannot find default branch for $address"
)
)
)
case Some(branch) =>
val fileRegex =
"""\{"name":"([^"]*?)","path":"([^"]*?)","contentType":"file"\}"""
@ -59,7 +67,7 @@ case class GithubHeuristic(info: DependencyInformation, log: Logger) {
.map(m => (m.group("name"), m.group("path")))
.filter(p => mayBeRelevant(p._1))
.toList
matches.flatMap { case (_, path) =>
val results = matches.map { case (_, path) =>
val rawHref = address + "/raw/" + branch + "/" + path
// This path is reconstructed to match the 'legacy' format for compatibility with older versions of the review settings.
// It has the format <org>/<repo>/blob/<branch>/<path>
@ -68,26 +76,41 @@ case class GithubHeuristic(info: DependencyInformation, log: Logger) {
.stripSuffix("/") + "/blob/" + branch + "/" + path
try {
val content = url(rawHref).cat.!!
Seq(
AttachedFile(
PortablePath.of(internalPath),
content,
origin = Some(address)
WithDiagnostics(
Seq(
AttachedFile(
PortablePath.of(internalPath),
content,
origin = Some(address)
)
)
)
} catch {
case NonFatal(error) =>
log.warn(
s"Found file $rawHref but cannot download it: $error"
WithDiagnostics(
Seq(),
Seq(
Diagnostic.Error(
s"GitHub heuristic failure: " +
s"Found file $rawHref but cannot download it: $error"
)
)
)
Seq()
}
}
results.flip.map(_.flatten)
}
} catch {
case NonFatal(error) =>
log.warn(s"GitHub backend for ${info.packageName} failed with $error")
Seq()
WithDiagnostics(
Seq(),
Seq(
Diagnostic.Error(
s"GitHub heuristic failure: " +
s"processing ${info.packageName} failed with error: $error"
)
)
)
}
/** Decides if the file may be relevant and should be included in the result.

View File

@ -11,13 +11,18 @@ import src.main.scala.licenses.DependencyInformation
*/
object DependencyFilter {
/** Decides if the dependency should be kept for further processing.
*/
/** Decides if the dependency should be kept for further processing. */
def shouldKeep(dependencyInformation: DependencyInformation): Boolean =
shouldKeep(dependencyInformation.moduleInfo)
/** Decides if the module should be kept for further processing.
*/
/** Decides if the module should be kept for further processing. */
def shouldKeep(moduleInfo: DepModuleInfo): Boolean =
moduleInfo.organization != "org.enso"
!shouldIgnore(moduleInfo)
def shouldIgnore(moduleInfo: DepModuleInfo): Boolean = {
val isEnsoModule = moduleInfo.organization == "org.enso"
val isGuavaEmptyPlaceholder =
moduleInfo.version == "9999.0-empty-to-avoid-conflict-with-guava"
isEnsoModule || isGuavaEmptyPlaceholder
}
}

View File

@ -1,20 +1,20 @@
package src.main.scala.licenses.frontend
import java.nio.file.Path
import sbtlicensereport.license.{DepLicense, DepModuleInfo}
import org.apache.ivy.core.resolve.IvyNode
import sbt.Compile
import sbt.internal.util.ManagedLogger
import sbt.io.IO
import sbt.librarymanagement.ConfigRef
import src.main.scala.licenses.report.Diagnostic
import src.main.scala.licenses.{
DependencyInformation,
SBTDistributionComponent,
SourceAccess
}
import scala.collection.JavaConverters._
import scala.collection.JavaConverters.*
/** Defines the algorithm for discovering dependency metadata.
*/
@ -44,8 +44,8 @@ object SbtLicenses {
def analyze(
components: Seq[SBTDistributionComponent],
log: ManagedLogger
): (Seq[DependencyInformation], Seq[String]) = {
val results: Seq[(Seq[Dependency], Vector[Path], Seq[String])] =
): (Seq[DependencyInformation], Seq[Diagnostic]) = {
val results: Seq[(Seq[Dependency], Vector[Path], Seq[Diagnostic])] =
components.map { component =>
val report = component.licenseReport.orig
val ivyDeps =
@ -73,12 +73,16 @@ object SbtLicenses {
Dependency(dep, depNode, sources)
}
val warnings =
val diagnostics =
if (component.licenseReport.licenses.isEmpty)
Seq(s"License report for component ${component.name} is empty.")
Seq(
Diagnostic.Error(
s"License report for component ${component.name} is empty."
)
)
else Seq()
(deps, sourceArtifacts, warnings)
(deps, sourceArtifacts, diagnostics)
}
val distinctDependencies =
@ -98,12 +102,14 @@ object SbtLicenses {
val missingWarnings = for {
dep <- relevantDeps
if dep.sources.isEmpty
} yield s"Could not find sources for ${dep.moduleInfo}"
} yield Diagnostic.Warning(s"Could not find sources for ${dep.moduleInfo}")
val unexpectedWarnings = for {
source <- distinctSources
if !distinctDependencies.exists(_.sourcesJARPaths.contains(source))
} yield s"Found a source $source that does not belong to any known " +
s"dependencies, perhaps the algorithm needs updating?"
} yield Diagnostic.Warning(
s"Found a source $source that does not belong to any known " +
s"dependencies, perhaps the algorithm needs updating?"
)
val reportsWarnings = results.flatMap(_._3)
(relevantDeps, missingWarnings ++ unexpectedWarnings ++ reportsWarnings)

View File

@ -0,0 +1,24 @@
package src.main.scala.licenses.report
sealed trait Diagnostic {
def message: String
}
object Diagnostic {
/** A warning indicating some unexpected event that should be noted
* but does not stop the report from being accepted.
*/
case class Warning(message: String) extends Diagnostic
/** An error with the license review that has to be resolved
* before the report can be accepted.
*/
case class Error(message: String) extends Diagnostic
def partition(diagnostics: Seq[Diagnostic]): (Seq[Warning], Seq[Error]) = {
val warnings = diagnostics.collect { case n: Warning => n }
val errors = diagnostics.collect { case p: Error => p }
(warnings, errors)
}
}

View File

@ -21,9 +21,10 @@ class HTMLWriter(bufferedWriter: BufferedWriter) {
s"""<html>
|<head>
|<meta charset="utf-8">
|<title>$title</title>
|<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
|<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|<title>$title</title>
|<link rel="stylesheet" href="https://code.jquery.com/ui/1.10.4/themes/ui-lightness/jquery-ui.css">
|<style>
|table, th, td {
| border: solid 1px;
@ -113,9 +114,12 @@ class HTMLWriter(bufferedWriter: BufferedWriter) {
* @param elements sequence of functions that will be called to write each of
* the list's elements; everything written inside of each
* function will be part of its list element
* @param addBullets specifies if the list should include bullet points for each entry
*/
def writeList(elements: Seq[() => Unit]): Unit = {
writer.println("<ul>")
def writeList(elements: Seq[() => Unit], addBullets: Boolean = true): Unit = {
val opening =
if (addBullets) "<ul>" else "<ul style=\"list-style-type: none;\">"
writer.println(opening)
for (elem <- elements) {
writer.println("<li>")
elem()

View File

@ -19,13 +19,13 @@ object Report {
*
* @param description description of the distribution
* @param summary reviewed summary of findings
* @param warnings sequence of warnings
* @param diagnostics sequence of diagnostics
* @param destination location of the generated HTML file
*/
def writeHTML(
description: DistributionDescription,
summary: ReviewedSummary,
warnings: Seq[String],
diagnostics: Seq[Diagnostic],
destination: File
): Unit = {
IO.createDirectory(destination.getParentFile)
@ -40,22 +40,32 @@ object Report {
s"${description.rootComponentsNames.mkString(", ")}."
)
if (warnings.isEmpty) {
writer.writeParagraph("There are no warnings.", Style.Green)
val (warnings: Seq[Diagnostic.Warning], errors: Seq[Diagnostic.Error]) =
Diagnostic.partition(diagnostics)
if (warnings.nonEmpty) {
writer.writeSubHeading("Warnings")
writer.writeList(warnings.map { notice => () =>
writer.writeText(notice.message)
})
}
if (errors.nonEmpty) {
writer.writeSubHeading(
f"There are ${errors.size} fatal-errors found in the review."
)
writer.writeList(errors.map { problem => () =>
writer.writeText(problem.message, Style.Red)
})
} else {
writer.writeParagraph(
s"There are ${warnings.size} warnings!",
Style.Bold,
Style.Red
"No fatal-errors found in the review.",
Style.Green
)
}
writeDependencySummary(writer, summary)
writer.writeList(warnings.map { warning => () =>
writer.writeText(writer.escape(warning))
})
writer.writeCollapsible("NOTICE header", summary.noticeHeader)
if (summary.additionalFiles.nonEmpty) {
writer.writeSubHeading("Additional files that will be added:")
@ -203,24 +213,28 @@ object Report {
rowWriter.addColumn {
if (files.isEmpty) writer.writeText("No attached files.")
else
writer.writeList(files.map { case (file, status) =>
() =>
val injection = writer.makeInjectionHandler(
"file-ui",
"package" -> dep.information.packageName,
"filename" -> file.path.toString,
"status" -> status.toString
)
val origin = file.origin
.map(origin => s" (Found at $origin)")
.getOrElse("")
writer.writeCollapsible(
s"${file.fileName} (${renderStatus(status)})$origin " +
s"${renderSimilarity(defaultLicense, file, status)}",
injection +
writer.escape(file.content)
)
})
writer.writeList(
files.map { case (file, status) =>
() =>
val injection = writer.makeInjectionHandler(
"file-ui",
"package" -> dep.information.packageName,
"filename" -> file.path.toString,
"status" -> status.toString
)
val origin = file.origin
.map(origin => s" (Found at $origin)")
.getOrElse("")
writer.writeCollapsible(
s"${file.fileName} (${renderStatus(status)})$origin " +
s"${renderSimilarity(defaultLicense, file, status)}",
injection +
writer.escape(file.content)
)
},
// Bullets are not needed because jQuery CSS will handle this better
addBullets = false
)
}
rowWriter.addColumn {
if (copyrights.isEmpty) {
@ -228,46 +242,50 @@ object Report {
if (bothEmpty) {
writer.writeText(
"No notices or copyright information found, " +
"this may be a problem.",
"this is a problem - add the information manually " +
"using `copyright-add` or `files-add`.",
Style.Red
)
} else {
writer.writeText("No copyright information found.")
}
} else
writer.writeList(copyrights.sortBy(_._1.content).map {
case (mention, status) =>
() =>
val foundAt = mention.origins match {
case Seq() => ""
case Seq(one) => s"Found at $one"
case Seq(first, rest @ _*) =>
s"Found at $first and ${rest.size} other files."
}
writer.writeList(
copyrights.sortBy(_._1.content).map {
case (mention, status) =>
() =>
val foundAt = mention.origins match {
case Seq() => ""
case Seq(one) => s"Found at $one"
case Seq(first, rest @ _*) =>
s"Found at $first and ${rest.size} other files."
}
val contexts = if (mention.contexts.nonEmpty) {
mention.contexts
.map(c => "\n" + writer.escape(c) + "\n")
.mkString("<hr>")
} else ""
val contexts = if (mention.contexts.nonEmpty) {
mention.contexts
.map(c => "\n" + writer.escape(c) + "\n")
.mkString("<hr>")
} else ""
val injection = writer.makeInjectionHandler(
"copyright-ui",
"package" -> dep.information.packageName,
"content" -> Base64.getEncoder.encodeToString(
mention.content.getBytes(StandardCharsets.UTF_8)
),
"contexts" -> mention.contexts.length.toString,
"status" -> status.toString
)
val injection = writer.makeInjectionHandler(
"copyright-ui",
"package" -> dep.information.packageName,
"content" -> Base64.getEncoder.encodeToString(
mention.content.getBytes(StandardCharsets.UTF_8)
),
"contexts" -> mention.contexts.length.toString,
"status" -> status.toString
)
val content = writer.escape(mention.content)
val moreInfo = injection + foundAt + contexts
writer.writeCollapsible(
s"$content (${renderStatus(status)})",
moreInfo
)
})
val content = writer.escape(mention.content)
val moreInfo = injection + foundAt + contexts
writer.writeCollapsible(
s"$content (${renderStatus(status)})",
moreInfo
)
}, // Bullets are not needed because jQuery CSS will handle this better
addBullets = false
)
}
}
)

View File

@ -71,7 +71,7 @@ case class Review(root: File, dependencySummary: DependencySummary) {
/** Runs the review process, returning a [[ReviewedDependency]] which includes
* information from the [[DependencySummary]] enriched with review statuses.
*/
def run(): WithWarnings[ReviewedSummary] =
def run(): WithDiagnostics[ReviewedSummary] =
for {
reviews <- dependencySummary.dependencies.map {
case (information, attachments) =>
@ -94,17 +94,19 @@ case class Review(root: File, dependencySummary: DependencySummary) {
*/
private def warnAboutMissingDependencies(
existingPackageNames: Seq[String]
): WithWarnings[Unit] = {
): WithDiagnostics[Unit] = {
val foundConfigurations = listFiles(root).filter(_.isDirectory)
val expectedFileNames =
existingPackageNames ++ Seq(Paths.filesAdd, Paths.reviewedLicenses)
val unexpectedConfigurations =
foundConfigurations.filter(p => !expectedFileNames.contains(p.getName))
val warnings = unexpectedConfigurations.map(p =>
s"Found legal review configuration for package ${p.getName}, " +
s"but no such dependency has been found. Perhaps it has been removed?"
val diagnostics = unexpectedConfigurations.map(p =>
Diagnostic.Error(
s"Found legal review configuration for package ${p.getName}, " +
s"but no such dependency has been found. Perhaps it has been removed or renamed (version change)?"
)
)
WithWarnings.justWarnings(warnings)
WithDiagnostics.justDiagnostics(diagnostics)
}
/** Finds a header defined in the settings or
@ -157,7 +159,7 @@ case class Review(root: File, dependencySummary: DependencySummary) {
private def reviewDependency(
info: DependencyInformation,
attachments: Seq[Attachment]
): WithWarnings[ReviewedDependency] = {
): WithDiagnostics[ReviewedDependency] = {
val packageRoot = root / info.packageName
val (files, copyrights) = splitAttachments(attachments)
val copyrightsDeduplicated =
@ -182,7 +184,7 @@ case class Review(root: File, dependencySummary: DependencySummary) {
private def reviewFiles(
packageRoot: File,
files: Seq[AttachedFile]
): WithWarnings[Seq[(AttachedFile, AttachmentStatus)]] = {
): WithDiagnostics[Seq[(AttachedFile, AttachmentStatus)]] = {
def keyForFile(file: AttachedFile): String = file.path.toString
val keys = files.map(keyForFile)
for {
@ -214,7 +216,7 @@ case class Review(root: File, dependencySummary: DependencySummary) {
private def reviewCopyrights(
packageRoot: File,
copyrights: Seq[CopyrightMention]
): WithWarnings[Seq[(CopyrightMention, AttachmentStatus)]] = {
): WithDiagnostics[Seq[(CopyrightMention, AttachmentStatus)]] = {
def keyForMention(copyrightMention: CopyrightMention): String =
copyrightMention.content.strip
val keys = copyrights.map(keyForMention)
@ -260,11 +262,11 @@ case class Review(root: File, dependencySummary: DependencySummary) {
private def reviewLicense(
packageRoot: File,
info: DependencyInformation
): WithWarnings[LicenseReview] =
): WithDiagnostics[LicenseReview] =
readFile(packageRoot / Paths.customLicense) match {
case Some(content) =>
val customFilename = content.strip()
WithWarnings(LicenseReview.Custom(customFilename))
WithDiagnostics(LicenseReview.Custom(customFilename))
case None =>
val directory = root / Paths.reviewedLicenses
val fileName = Review.normalizeName(info.license.name)
@ -274,16 +276,20 @@ case class Review(root: File, dependencySummary: DependencySummary) {
readFile(settingPath)
.map { content =>
if (content.isBlank) {
WithWarnings(
WithDiagnostics(
LicenseReview.NotReviewed,
Seq(s"License review file $settingPath is empty.")
Seq(
Diagnostic.Error(
s"License review file $settingPath is empty, but it should contain a path to a license text inside of `license-texts`."
)
)
)
} else
try {
val path = Path.of(content.strip())
val bothDefaultAndCustom =
(packageRoot / Paths.defaultAndCustomLicense).exists()
WithWarnings(
WithDiagnostics(
LicenseReview.Default(
path,
allowAdditionalCustomLicenses = bothDefaultAndCustom
@ -291,24 +297,34 @@ case class Review(root: File, dependencySummary: DependencySummary) {
)
} catch {
case e: InvalidPathException =>
WithWarnings(
WithDiagnostics(
LicenseReview.NotReviewed,
Seq(
s"License review file $settingPath is malformed: $e"
Diagnostic.Error(
s"License review file $settingPath is malformed: $e"
)
)
)
}
}
.getOrElse(WithWarnings(LicenseReview.NotReviewed))
.getOrElse(WithDiagnostics(LicenseReview.NotReviewed))
case Array(_, _*) =>
WithWarnings(
WithDiagnostics(
LicenseReview.NotReviewed,
Seq(s"Multiple copies of file $settingPath with differing case.")
Seq(
Diagnostic.Error(
s"Multiple copies of file $settingPath with differing case (the license names are matched case insensitively)."
)
)
)
case Array() =>
WithWarnings(
WithDiagnostics(
LicenseReview.NotReviewed,
Seq(s"License review file $settingPath is missing.")
Seq(
Diagnostic.Error(
s"License review file $settingPath is missing. Either review the default license or set a `custom-license` for packages that used it."
)
)
)
}
}
@ -328,16 +344,18 @@ case class Review(root: File, dependencySummary: DependencySummary) {
fileName: String,
expectedLines: Seq[String],
packageRoot: File
): WithWarnings[Seq[String]] = {
): WithDiagnostics[Seq[String]] = {
val lines = readLines(packageRoot / fileName)
val unexpectedLines = lines.filter(l => !expectedLines.contains(l))
val warnings = unexpectedLines.map(l =>
s"File $fileName in ${packageRoot.getName} contains entry `$l`, but no " +
s"such entry has been detected. Perhaps it has disappeared after an " +
s"update? Please remove it from the file and make sure that the report " +
s"contains all necessary elements after this change."
Diagnostic.Error(
s"File $fileName in ${packageRoot.getName} contains entry `$l`, but no " +
s"such entry has been detected. Perhaps it has disappeared after an " +
s"update? Please remove it from the file and make sure that the report " +
s"contains all necessary elements after this change."
)
)
WithWarnings(lines, warnings)
WithDiagnostics(lines, warnings)
}
/** Reads the file as a [[String]].

View File

@ -0,0 +1,57 @@
package src.main.scala.licenses.report
/** A simple monad for storing diagnostics related to a result.
*/
case class WithDiagnostics[+A](value: A, diagnostics: Seq[Diagnostic] = Seq()) {
/** Returns a result with a mapped value and the same diagnostics.
*/
def map[B](f: A => B): WithDiagnostics[B] =
WithDiagnostics(f(value), diagnostics)
/** Combines two computations returning diagnostics, preserving diagnostics from
* both of them.
*/
def flatMap[B](f: A => WithDiagnostics[B]): WithDiagnostics[B] = {
val result = f(value)
WithDiagnostics(result.value, diagnostics ++ result.diagnostics)
}
}
object WithDiagnostics {
implicit class SeqWithDiagnosticsSyntax[+A](seq: Seq[WithDiagnostics[A]]) {
/** Turns a sequence of [[WithDiagnostics]] instances into a single
* [[WithDiagnostics]] that contains the sequence of values and combined
* diagnostics.
*/
def flip: WithDiagnostics[Seq[A]] =
WithDiagnostics(seq.map(_.value), seq.flatMap(_.diagnostics))
}
implicit class SeqLikeSyntax[A](seq: WithDiagnostics[Seq[A]]) {
/** A shorthand syntax to concatenate two sequences wrapped with diagnostics,
* combining their diagnostics.
*/
def ++(other: WithDiagnostics[Seq[A]]): WithDiagnostics[Seq[A]] =
for {
lhs <- seq
rhs <- other
} yield lhs ++ rhs
/** A shorthand syntax to concatenate an ordinary sequence to a sequence
* with diagnostics.
*/
def ++(other: Seq[A]): WithDiagnostics[Seq[A]] =
for {
lhs <- seq
} yield lhs ++ other
}
/** Creates a [[WithDiagnostics]] containing Unit and a provided sequence of
* diagnostics.
*/
def justDiagnostics(diagnostics: Seq[Diagnostic]): WithDiagnostics[Unit] =
WithDiagnostics((), diagnostics)
}

View File

@ -1,56 +0,0 @@
package src.main.scala.licenses.report
/** A simple monad for storing warnings related to a result.
*/
case class WithWarnings[+A](value: A, warnings: Seq[String] = Seq()) {
/** Returns a result with a mapped value and the same warnings.
*/
def map[B](f: A => B): WithWarnings[B] = WithWarnings(f(value), warnings)
/** Combines two computations returning warnings, preserving warnings from
* both of them.
*/
def flatMap[B](f: A => WithWarnings[B]): WithWarnings[B] = {
val result = f(value)
WithWarnings(result.value, warnings ++ result.warnings)
}
}
object WithWarnings {
implicit class SeqWithWarningsSyntax[+A](seq: Seq[WithWarnings[A]]) {
/** Turns a sequence of [[WithWarnings]] instances into a single
* [[WithWarnings]] that contains the sequence of values and combined
* warnings.
*/
def flip: WithWarnings[Seq[A]] =
WithWarnings(seq.map(_.value), seq.flatMap(_.warnings))
}
implicit class SeqLikeSyntax[A](seq: WithWarnings[Seq[A]]) {
/** A shorthand syntax to concatenate two sequences wrapped with warnings,
* combining their warnings.
*/
def ++(other: WithWarnings[Seq[A]]): WithWarnings[Seq[A]] =
for {
lhs <- seq
rhs <- other
} yield lhs ++ rhs
/** A shorthand syntax to concatenate an ordinary sequence to a sequence
* with warnings.
*/
def ++(other: Seq[A]): WithWarnings[Seq[A]] =
for {
lhs <- seq
} yield lhs ++ other
}
/** Creates a [[WithWarnings]] containing Unit and a provided sequence of
* warnings.
*/
def justWarnings(warnings: Seq[String]): WithWarnings[Unit] =
WithWarnings((), warnings)
}

View File

@ -42,10 +42,10 @@ function makeHandler(elem, data, file, action) {
$(function () {
$('body').prepend(
'<div style="color:red">This review helper tool does not regenerate the ' +
'report - to see the changes that are applied using this tool after ' +
'refreshing the page, you need to regenerate the report using the ' +
'`gatherLicenses` command.</div>'
'<div style="color:orange">This review helper tool does not regenerate the ' +
'report - any changes that are applied using this tool will not be visible after ' +
'refreshing the page, until you regenerate the report by running ' +
'`openLegalReviewReport` command again.</div>'
)
$('body').append(
'<div id="status" ' + 'style="position: fixed;left:4pt;bottom:4pt">' + 'Loading...</div>'

View File

@ -1 +0,0 @@
See com.google.guava.guava-32.1.3-jre for licensing information.

View File

@ -1,3 +1,3 @@
DB2DF2FB35D384E72823D9658B4322408922D661D353D3770002DA293F0610DD
023B7EC2B582D930CD7FCAC326C0801D1945D341AEB004087246878882B49DAC
DBDDDBEE67512DE9CAA6EFE1BAF08100C4EE0D25CD033E3B3049C5B66EB4BC84
A0216475A3111DAC8C4B66D612E5563631D628C3230FFEF31DA3C0A0BB5AE2B1
0

View File

@ -1 +0,0 @@
See com.google.guava.guava-29.0-jre for licensing information.

View File

@ -1,3 +1,3 @@
35A1500A3A4AF8131C4F3FB5BD818C9DEE8F503D6AA83723AA3C87765480CA58
E18068AE6E3410A4876AB626424798EE1F4845F6D94227DEC2D872BFFBC28E82
911AAD4DA796BD9B80E14DE7DF556BE853E6C744FAC0B07C1BEECE86F1E43FD9
90B547E0CB6962C580225A770AE08EB2F17B94115ED78C888036C27753A62CB2
0