diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 651260c..958b513 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -63,14 +63,24 @@ jobs: run: | shopt -s globstar - license-scanner scan \ + ! license-scanner scan \ --ensure-licenses Apache-2.0 GPL-3.0-only \ --exclude ./**/target ./**/weights \ -- ./**/src/**/*.rs \ - 2>out.txt \ - && exit 1 || exit 0 + 2>out.txt # We expected it to fail because there are some unlicensed files left. grep -q 'No license detected in reconstruct.rs. Exact file path:' ./out.txt grep -q 'No license detected in mod.rs. Exact file path:' ./out.txt working-directory: polkadot + - name: Enforce product references in headers + run: | + ! license-scanner scan \ + --ensure-product 'Polkadot' \ + -- ./xcm/src/lib.rs \ + 2>out.txt + # We expected it to fail because there are some copy-paste errors. + + grep -q 'Product mismatch' ./out.txt + grep -q 'Expected "Polkadot", detected "Substrate" in line:' ./out.txt + working-directory: polkadot diff --git a/README.md b/README.md index af2c281..7a3d6d0 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,28 @@ yarn start -- scan --ensure-any-license /directory/or/file Those options are conflicting with each other so only one should be specified. By default, no licensing is enforced. +## `--ensure-product` + +If configured, the scan will make sure that if a license header references a product, +it will be the correct product and not a result of a copy-paste error. + +For example, this fragment references the `Substrate` product. + +```text +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +``` + +Examples: + +```bash +yarn start -- scan --ensure-product Polkadot -- /directory/or/file +``` + +It treats a different product reference as an error, but it allows a generic "this program". + ## `--exclude` Can be used to exclude files or directories from the scan. diff --git a/license-scanner/cli/scan.ts b/license-scanner/cli/scan.ts index 7998716..f0d0a14 100644 --- a/license-scanner/cli/scan.ts +++ b/license-scanner/cli/scan.ts @@ -86,6 +86,7 @@ export const executeScan = async function ({ detectionOverrides, logLevel, ensureLicenses, + ensureProduct, exclude, }: ScanCliArgs) { const licenses = await loadLicensesNormalized(joinPath(buildRoot, "licenses"), { @@ -119,6 +120,7 @@ export const executeScan = async function ({ detectionOverrides: detectionOverrides ?? null, logger, ensureLicenses, + ensureProduct, }); allLicensingErrors.push(...licensingErrors); } diff --git a/license-scanner/license.ts b/license-scanner/license.ts index bc15553..ebe8717 100644 --- a/license-scanner/license.ts +++ b/license-scanner/license.ts @@ -299,6 +299,34 @@ export const ensureLicensesInResult = function ({ } }; +/** + * If a product is mentioned in this file, + * ensure that it is the correct product, + * and not a copy-paste error from a different product. + */ +export const ensureProductInFile = function (filePath: string, product: string | undefined): Error | undefined { + if (!product) return; + const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/); + for (const regexp of [ + new RegExp("This file is part of (.*)\\."), + new RegExp("// (.*) is free software"), + new RegExp("// (.*) is distributed in the hope"), + new RegExp("// along with (.+?)\\.(.*)gnu.org"), + ]) { + for (const line of lines) { + if (regexp.test(line)) { + const matches = regexp.exec(line); + assert(matches); + if (matches[1] !== product && matches[1].toLowerCase() !== "this program") { + return new Error( + `Product mismatch in ${filePath}. Expected "${product}", detected "${matches[1]}" in line: "${line}".`, + ); + } + } + } + } +}; + export const throwLicensingErrors = function (licensingErrors: Error[]) { if (licensingErrors.length === 0) return; throw new Error( diff --git a/license-scanner/main.ts b/license-scanner/main.ts index a5fe90b..cdedc88 100755 --- a/license-scanner/main.ts +++ b/license-scanner/main.ts @@ -47,6 +47,12 @@ program "If configured, the scan will make sure that all scanned files are licensed with any license.", ).conflicts("ensureLicenses"), ) + .addOption( + new Option( + "--ensure-product ", + "If configured, the scan will make sure the product mentioned in the license headers is correct.", + ), + ) .option("--exclude ", "Can be used to exclude files or directories from the scan.") // It's actually correct usage but @commander-js/extra-typings is wrong on this one. // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -60,6 +66,7 @@ program exclude: options.exclude ?? [], logLevel: options.logLevel as LogLevel, ensureLicenses: readEnsureLicenses(options), + ensureProduct: options.ensureProduct, }); } catch (e: any) { logger.debug(e.stack); diff --git a/license-scanner/scanner.ts b/license-scanner/scanner.ts index cbdb706..afd5015 100644 --- a/license-scanner/scanner.ts +++ b/license-scanner/scanner.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { dirname, join as joinPath, relative as relativePath } from "path"; import { getOrDownloadCrate, getVersionedCrateName } from "./crate"; -import { ensureLicensesInResult } from "./license"; +import { ensureLicensesInResult, ensureProductInFile } from "./license"; import { getOrDownloadRepository } from "./repository"; import { scanQueue, scanQueueSize } from "./synchronization"; import { @@ -133,6 +133,7 @@ export const scan = async function (options: ScanOptions): Promise { tracker, logger, ensureLicenses = false, + ensureProduct, } = options; const licensingErrors: Error[] = []; @@ -179,6 +180,8 @@ export const scan = async function (options: ScanOptions): Promise { const result = await matchLicense(file.path); const licensingError = ensureLicensesInResult({ file, result, ensureLicenses }); if (licensingError) licensingErrors.push(licensingError); + const productError = ensureProductInFile(file.path, ensureProduct); + if (productError) licensingErrors.push(productError); if (result === undefined) { return; } diff --git a/license-scanner/types.ts b/license-scanner/types.ts index 0ff1c0e..4746224 100644 --- a/license-scanner/types.ts +++ b/license-scanner/types.ts @@ -77,6 +77,11 @@ export type ScanOptions = { * all source files have one of those licenses detected. */ ensureLicenses?: boolean | string[]; + /** + * If true, the scan will make sure that + * the license headers contain the correct product name. + */ + ensureProduct?: string | undefined; }; export type LicenseInput = { @@ -150,6 +155,7 @@ export interface ScanCliArgs { detectionOverrides: DetectionOverride[]; logLevel: LogLevel; ensureLicenses: boolean | string[]; + ensureProduct: string | undefined; } export interface DumpCliArgs {