mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-07-16 17:26:58 -04:00
## What & why We used to enforce the BSD-2-Clause license banner on every source file via `eslint-plugin-header`. That check was lost when we migrated from ESLint to Biome (#7033), which has no equivalent rule. This PR reinstates it as a standalone node script, modelled on the existing `etc/scripts/check-frontend-imports.js`. ### Why a script rather than a Biome rule Biome 2.x has no built-in license/header rule and no plugin equivalent to `eslint-plugin-header`; its experimental GritQL plugins aren't suited to whole-file-prefix matching. A script needs zero new dependencies and gives full control over scope and exemptions. ## The check (`etc/scripts/check-license-headers.js`) A file "has an appropriate banner" if, ignoring an optional shebang, it opens with a `// Copyright (c) …` line **and** contains the BSD-2-Clause disclaimer body. The year and copyright holder are intentionally **not** constrained — the tree legitimately has many holders (Compiler Explorer Authors, Arm, Microsoft, HRT, individuals). The `(c)`/`(C)` marker is matched case-insensitively. **Scope:** `.ts/.js/.mjs/.cjs` under `lib/ static/ shared/ types/ test/ cypress/`. **Exempt:** generated files (`lib/asm-docs/generated`), vendored (`docenizer/vendor`), `.d.ts`, and three third-party ports that carry their own upstream license — `static/ansi-to-html.ts` (MIT), `lib/node-graceful.ts` (MIT), `shared/rison.ts` (Nanonid/rison port). **Wired into:** CI (`test-and-deploy.yml`), `npm run check`, the `make pre-commit` target, the husky pre-commit hook, and `lint-staged` (per staged file). Usage: ``` node ./etc/scripts/check-license-headers.js # scan the tracked tree node ./etc/scripts/check-license-headers.js <files...> # scan specific files (lint-staged) ``` ## Backfill The check surfaced **42 CE-authored files** missing the banner. This PR backfills them all with the standard `Copyright (c) <year>, Compiler Explorer Authors` banner, using each file's **git creation year** (added-at-this-path, so no `--follow` rename artifacts). ## Verification - `check-license-headers` → clean (was 42 failures) - `biome check` on all source → no fixes needed (banner format matches existing convention) - `tsc` backend + frontend + tests → clean - pre-commit gauntlet (lint, ts-check, related tests: 528 passed) ran on commit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mattgodbolt-molty <mattgodbolt-molty@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
4.8 KiB
JavaScript
105 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// Check that every source file starts with the BSD-2-Clause license banner.
|
|
//
|
|
// We used to enforce this via eslint-plugin-header; that check was lost in the
|
|
// migration to Biome (which has no equivalent rule). This is a standalone
|
|
// replacement, run in CI, in the husky pre-commit hook, and by `make pre-commit`.
|
|
//
|
|
// Usage:
|
|
// node ./etc/scripts/check-license-headers.js # scan the tracked tree
|
|
// node ./etc/scripts/check-license-headers.js <files...> # scan just these files
|
|
|
|
import {execSync} from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
// Directories/globs whose source files must carry the banner.
|
|
const INCLUDE_DIRS = ['lib', 'static', 'shared', 'types', 'test', 'cypress'];
|
|
const INCLUDE_EXTENSIONS = ['.ts', '.js', '.mjs', '.cjs'];
|
|
|
|
// Files/paths that are exempt (generated, vendored, or third-party).
|
|
const EXCLUDE_PATTERNS = [
|
|
/(^|\/)node_modules\//,
|
|
/(^|\/)out\//,
|
|
/^lib\/asm-docs\/generated\//,
|
|
/^etc\/scripts\/docenizer\/vendor\//,
|
|
/\.d\.ts$/,
|
|
// Third-party code carrying its own upstream license (not CE's BSD-2 banner).
|
|
/^static\/ansi-to-html\.ts$/, // MIT, Rob Burns
|
|
/^lib\/node-graceful\.ts$/, // MIT, node-graceful
|
|
/^shared\/rison\.ts$/, // Nanonid/rison port
|
|
// CE-authored files that pre-date this check and carry an MIT header rather than
|
|
// the repo-standard BSD-2. Exempt for now; relicensing them to BSD-2 is a separate
|
|
// call for a maintainer.
|
|
/^lib\/stack-usage-transformer\.ts$/,
|
|
/^test\/stack-usage-transformer-test\.ts$/,
|
|
/^test\/library-tests\.ts$/,
|
|
];
|
|
|
|
// A file "has an appropriate banner" if, ignoring an optional shebang, it opens
|
|
// with a `// Copyright (c) ...` line and contains the BSD-2-Clause disclaimer.
|
|
// The year and copyright holder are intentionally not constrained: the tree has
|
|
// many holders (Compiler Explorer Authors, Arm Ltd, Microsoft, individuals, ...).
|
|
// No /m flag: the copyright line must be the very first line (post-shebang), not
|
|
// merely present somewhere in the head.
|
|
const COPYRIGHT_RE = /^\/\/ Copyright \([cC]\) .+/;
|
|
// Two distinctive anchors from the BSD-2-Clause body: the opening grant and the
|
|
// disclaimer line. Requiring both (rather than a single line) avoids passing a file
|
|
// that merely happens to contain one stray phrase. We don't match the whole banner
|
|
// verbatim — that would be brittle to minor punctuation/quoting differences (e.g. a
|
|
// couple of files quote AS IS with single quotes, which is why that part is lenient).
|
|
const REDISTRIBUTION_RE = /Redistribution and use in source and binary forms/;
|
|
const DISCLAIMER_RE = /THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ["']AS IS["']/;
|
|
|
|
// Normalise to a repo-relative POSIX path so matching works whether we're handed
|
|
// tracked paths from `git ls-files` or absolute/backslashed paths from lint-staged.
|
|
function toRepoRelative(file) {
|
|
return path.relative(process.cwd(), path.resolve(file)).split(path.sep).join('/');
|
|
}
|
|
|
|
function isIncluded(file) {
|
|
if (!INCLUDE_EXTENSIONS.some(ext => file.endsWith(ext))) return false;
|
|
if (EXCLUDE_PATTERNS.some(re => re.test(file))) return false;
|
|
return INCLUDE_DIRS.some(dir => file === dir || file.startsWith(`${dir}/`));
|
|
}
|
|
|
|
function listTrackedFiles() {
|
|
return execSync('git ls-files', {encoding: 'utf8', maxBuffer: 64 * 1024 * 1024})
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function hasBanner(file) {
|
|
// Only the head of the file matters; read enough to cover the banner.
|
|
let head;
|
|
try {
|
|
const buf = Buffer.alloc(4096);
|
|
const fd = fs.openSync(file, 'r');
|
|
try {
|
|
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
head = buf.toString('utf8', 0, bytesRead);
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
} catch {
|
|
return true; // Unreadable/deleted file: not our problem to report here.
|
|
}
|
|
// Skip an optional shebang line so scripts like this one still qualify.
|
|
const body = head.startsWith('#!') ? head.slice(head.indexOf('\n') + 1) : head;
|
|
return COPYRIGHT_RE.test(body) && REDISTRIBUTION_RE.test(body) && DISCLAIMER_RE.test(body);
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
const candidates = (args.length > 0 ? args : listTrackedFiles()).map(toRepoRelative).filter(isIncluded);
|
|
const missing = candidates.filter(file => fs.existsSync(file) && !hasBanner(file));
|
|
|
|
if (missing.length > 0) {
|
|
console.error(`❌ ${missing.length} file(s) are missing the license header banner:\n`);
|
|
for (const file of missing) console.error(` ${file}`);
|
|
console.error('\nEvery source file must begin with the BSD-2-Clause banner (see any existing file for the');
|
|
console.error('exact text). The copyright year/holder line is flexible; the disclaimer body is required.');
|
|
process.exit(1);
|
|
}
|
|
|
|
process.exit(0);
|