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 {