#111 shipped 8 bad-auto-merge scars into the SPA past a CI that only ever lints/tests `src/` — the 27k-line docs/index.html had (and still has) no static gate. The parse test added with the #111 fix catches outright syntax failures; this adds the *semantic* layer that parsing misses. It runs ESLint's high-precision "possible problem" rules programmatically over each inline <script> (no new dependency — `eslint` is already a devDependency; no eslint.config or CI change — rides the existing `npm test`): no-redeclare, no-unreachable, no-dupe-keys, no-dupe-args, no-dupe-else-if, no-duplicate-case, no-const-assign, no-import-assign, no-class-assign — the exact shapes a conflict-concatenating merge leaves behind. Tight, always-a-bug rules, each script linted in isolation → zero false positives on the real file. no-undef / no-unused-vars are excluded (the SPA relies on cross-<script> and browser globals a per-block lint can't see) and no-func-assign is excluded (the app legitimately monkey-patches navigateTo). Verified: 0 findings on the fixed tree, and it rejects the pre-fix main (catches the duplicate `response`). Full suite green. Also drops a now-unused eslint-disable directive in the parse test.
This commit is contained in:
1 parent
7824041c26
commit
32b81708ea
1 file changed
+49
-1
@@ -14,6 +14,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import { Linter } from 'eslint';
|
||||
|
||||
const html = readFileSync(new URL('../../docs/index.html', import.meta.url), 'utf8');
|
||||
|
||||
@@ -44,7 +45,6 @@ describe('docs/index.html inline scripts (issue #111 regression)', () => {
|
||||
const failures: string[] = [];
|
||||
blocks.forEach((src, i) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new vm.Script(src, { filename: `docs/index.html#inline-${i}` });
|
||||
} catch (e) {
|
||||
failures.push(`inline #${i}: ${(e as Error).message}`);
|
||||
@@ -53,3 +53,51 @@ describe('docs/index.html inline scripts (issue #111 regression)', () => {
|
||||
expect(failures, failures.join('\n')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Second layer — semantic defect gate (#111 hardening).
|
||||
*
|
||||
* The vm.Script check above catches outright *parse* failures. This catches the
|
||||
* shapes a conflict-concatenating auto-merge leaves behind that still PARSE but are
|
||||
* always bugs — duplicate declarations, code after a `return`, duplicate object
|
||||
* keys — using ESLint's high-precision "possible problem" rules run programmatically.
|
||||
* No new dependency (`eslint` is already a devDependency) and no eslint.config / CI
|
||||
* change: this rides the existing `npm test`. #111 shipped 8 such scars past a CI
|
||||
* that only ever looked at `src/`; this closes that blind spot for the UI too.
|
||||
*
|
||||
* The rule set is deliberately a tight, always-a-bug set, and each `<script>` is
|
||||
* linted in isolation, so it is false-positive-free on the real file. Notably
|
||||
* EXCLUDED, by design:
|
||||
* - `no-undef` / `no-unused-vars` — the SPA relies on hundreds of cross-<script>
|
||||
* and browser globals a per-block lint can't see; enabling them would flood.
|
||||
* - `no-func-assign` — the app legitimately monkey-patches `navigateTo` (wraps it
|
||||
* to add CTF-range init), a pattern that rule flags.
|
||||
*/
|
||||
const SCAR_RULES: Linter.RulesRecord = {
|
||||
'no-redeclare': 'error',
|
||||
'no-unreachable': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
'no-dupe-args': 'error',
|
||||
'no-dupe-else-if': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-const-assign': 'error',
|
||||
'no-import-assign': 'error',
|
||||
'no-class-assign': 'error',
|
||||
};
|
||||
|
||||
describe('docs/index.html inline scripts — semantic defect gate (#111 hardening)', () => {
|
||||
it('no duplicate declarations, unreachable code, or duplicate keys in any inline <script>', () => {
|
||||
const linter = new Linter();
|
||||
const config: Linter.Config = {
|
||||
languageOptions: { ecmaVersion: 'latest', sourceType: 'script' },
|
||||
rules: SCAR_RULES,
|
||||
};
|
||||
const findings: string[] = [];
|
||||
inlineScripts(html).forEach((src, i) => {
|
||||
for (const m of linter.verify(src, config)) {
|
||||
findings.push(`inline #${i} L${m.line}:${m.column} [${m.ruleId ?? 'parse'}] ${m.message}`);
|
||||
}
|
||||
});
|
||||
expect(findings, findings.join('\n')).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user