#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 { describe, it, expect } from 'vitest';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import vm from 'node:vm';
|
import vm from 'node:vm';
|
||||||
|
import { Linter } from 'eslint';
|
||||||
|
|
||||||
const html = readFileSync(new URL('../../docs/index.html', import.meta.url), 'utf8');
|
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[] = [];
|
const failures: string[] = [];
|
||||||
blocks.forEach((src, i) => {
|
blocks.forEach((src, i) => {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line no-new
|
|
||||||
new vm.Script(src, { filename: `docs/index.html#inline-${i}` });
|
new vm.Script(src, { filename: `docs/index.html#inline-${i}` });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
failures.push(`inline #${i}: ${(e as Error).message}`);
|
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([]);
|
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