mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
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:
parent
edb349f8fc
commit
47c64167ef
@ -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() {
|
||||
|
@ -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`.
|
||||
|
@ -1,2 +0,0 @@
|
||||
See com.google.guava.guava-29.0-jre for licensing information.
|
||||
|
@ -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`.
|
||||
|
@ -1,2 +0,0 @@
|
||||
See com.google.guava.guava-32.1.3-jre for licensing information.
|
||||
|
@ -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`."
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
24
project/src/main/scala/licenses/report/Diagnostic.scala
Normal file
24
project/src/main/scala/licenses/report/Diagnostic.scala
Normal 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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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]].
|
||||
|
57
project/src/main/scala/licenses/report/WithDiagnostics.scala
Normal file
57
project/src/main/scala/licenses/report/WithDiagnostics.scala
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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>'
|
||||
|
@ -1 +0,0 @@
|
||||
See com.google.guava.guava-32.1.3-jre for licensing information.
|
@ -1,3 +1,3 @@
|
||||
DB2DF2FB35D384E72823D9658B4322408922D661D353D3770002DA293F0610DD
|
||||
023B7EC2B582D930CD7FCAC326C0801D1945D341AEB004087246878882B49DAC
|
||||
DBDDDBEE67512DE9CAA6EFE1BAF08100C4EE0D25CD033E3B3049C5B66EB4BC84
|
||||
A0216475A3111DAC8C4B66D612E5563631D628C3230FFEF31DA3C0A0BB5AE2B1
|
||||
0
|
||||
|
@ -1 +0,0 @@
|
||||
See com.google.guava.guava-29.0-jre for licensing information.
|
@ -1,3 +1,3 @@
|
||||
35A1500A3A4AF8131C4F3FB5BD818C9DEE8F503D6AA83723AA3C87765480CA58
|
||||
E18068AE6E3410A4876AB626424798EE1F4845F6D94227DEC2D872BFFBC28E82
|
||||
911AAD4DA796BD9B80E14DE7DF556BE853E6C744FAC0B07C1BEECE86F1E43FD9
|
||||
90B547E0CB6962C580225A770AE08EB2F17B94115ED78C888036C27753A62CB2
|
||||
0
|
||||
|
Loading…
Reference in New Issue
Block a user