mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 22:14:40 +00:00
feat(lint): P7 enforcement gates (no-new-throw-error + max-lines) + docs
Epic P7. Locks in the epic's gains with ESLint gates so the adoption work does not regress. - ccs/no-new-throw-error (error): custom flat-config rule that flags NEW throw new Error(...) outside a generated baseline allowlist. Forces the typed-error taxonomy (src/errors/error-types.ts). Existing 338 sites are grandfathered in eslint-rules/throw-error-baseline.json; only NEW violations error. Rule normalizes the filename to repo-root-relative to match baseline keys regardless of how ESLint reports paths. - max-lines (warn, 400, skipBlankLines/skipComments): warns on files over 400 LOC (goal of P5/P6 god-file splits). Currently 51 warnings on the not-yet-split god-files (P6 territory). - scripts/generate-throw-error-baseline.js: emits the allowlist from raw source (superset of real throws; never undercounts). Run after intentionally grandfathering a site, or quarterly to prune. - tests/unit/eslint-rules/no-throw-new-error.test.ts: rule logic (flags off-allowlist, passes on-allowlist/typed/rethrow, line-sensitivity). - docs/code-standards.md: Lint Enforcement Gates section. - docs/logging-contract.md: error.code -> ExitCode table (from P4). validate + validate:ci-parity green.
This commit is contained in:
1 parent
919be3c722
commit
2f94f35ec3
7 files changed
+642
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
const rule = require('../../../eslint-rules/no-new-throw-error.js');
|
||||
|
||||
/**
|
||||
* P7 rule test: no-new-throw-error flags NEW throw new Error(...) sites not in
|
||||
* the baseline allowlist, while grandfathered sites, typed throws, and
|
||||
* re-throws pass.
|
||||
*
|
||||
* Exercises the rule's create() directly with a mock context and AST node so
|
||||
* the test does not depend on the ESLint runtime under bun. The live
|
||||
* `bun run lint` already proves the ESLint integration end-to-end (0 violations
|
||||
* against the generated baseline).
|
||||
*/
|
||||
describe('ccs/no-new-throw-error rule logic', () => {
|
||||
const FILENAME = 'src/sample/fixture.ts';
|
||||
|
||||
function node(argument: unknown, line: number) {
|
||||
return {
|
||||
type: 'ThrowStatement',
|
||||
loc: { start: { line, column: 0 } },
|
||||
argument,
|
||||
};
|
||||
}
|
||||
|
||||
function runRule(throwNode: unknown, allowlist: string[]): Array<{ messageId: string }> {
|
||||
const reports: Array<{ messageId: string }> = [];
|
||||
const context = {
|
||||
options: [{ allowlist }],
|
||||
getFilename: () => FILENAME,
|
||||
report: (r: { messageId: string }) => reports.push(r),
|
||||
};
|
||||
const visitors = rule.create(context);
|
||||
visitors.ThrowStatement(throwNode);
|
||||
return reports;
|
||||
}
|
||||
|
||||
const newErrorArg = { type: 'NewExpression', callee: { type: 'Identifier', name: 'Error' } };
|
||||
const newTypedArg = { type: 'NewExpression', callee: { type: 'Identifier', name: 'ConfigError' } };
|
||||
const rethrowArg = { type: 'Identifier', name: 'err' };
|
||||
|
||||
test('flags a throw new Error off the allowlist', () => {
|
||||
expect(runRule(node(newErrorArg, 1), []).some((r) => r.messageId === 'unexpected')).toBe(true);
|
||||
});
|
||||
|
||||
test('passes a throw new Error that is on the allowlist (file:line match)', () => {
|
||||
expect(runRule(node(newErrorArg, 1), [`${FILENAME}:1`]).some((r) => r.messageId === 'unexpected')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('flags when the throw line differs from the allowlisted line', () => {
|
||||
expect(runRule(node(newErrorArg, 2), [`${FILENAME}:1`]).some((r) => r.messageId === 'unexpected')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('passes typed throws (ConfigError / AuthError)', () => {
|
||||
const typed = { type: 'NewExpression', callee: { type: 'Identifier', name: 'AuthError' } };
|
||||
expect(runRule(node(typed, 1), []).some((r) => r.messageId === 'unexpected')).toBe(false);
|
||||
expect(runRule(node(newTypedArg, 1), []).some((r) => r.messageId === 'unexpected')).toBe(false);
|
||||
});
|
||||
|
||||
test('passes re-throws and non-NewExpression throws', () => {
|
||||
expect(runRule(node(rethrowArg, 1), []).some((r) => r.messageId === 'unexpected')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user