Simplify local detector dispatch (#577)

* Simplify local detector dispatch

Centralize HTML-versus-text file routing for stdin, directory, and direct-file scans. Add CLI characterization coverage for both stdin paths.

Prepared with AI assistance under maintainer pbakaus's standing scheduled-refactor authorization.

* Strengthen detector dispatch characterization

Put the HTML-only finding in a linked stylesheet so the text engine cannot satisfy the static-engine assertion.

Prepared with AI assistance under maintainer pbakaus's standing scheduled-refactor authorization.
This commit is contained in:
Paul Bakaus
2026-08-14 05:45:42 -07:00
committed by GitHub
parent c7588067a4
commit a98ee8b10e
3 changed files with 76 additions and 16 deletions
+10 -16
View File
@@ -105,6 +105,13 @@ function formatFindings(findings, jsonMode) {
// `optionsFor` maps a local path to scan options carrying that path's own
// project design system (or base options when null). Falls back to a plain
// object so direct/legacy callers still work.
async function detectLocalFile(filePath, options) {
if (HTML_EXTENSIONS.has(path.extname(filePath).toLowerCase())) {
return detectHtml(filePath, options);
}
return detectText(fs.readFileSync(filePath, 'utf-8'), filePath, options);
}
async function handleStdin(optionsFor = () => ({})) {
const resolve = typeof optionsFor === 'function' ? optionsFor : () => optionsFor;
const chunks = [];
@@ -114,9 +121,7 @@ async function handleStdin(optionsFor = () => ({})) {
const parsed = JSON.parse(input);
const fp = parsed?.tool_input?.file_path;
if (fp && fs.existsSync(fp)) {
const options = resolve(fp);
return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase())
? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options);
return detectLocalFile(fp, resolve(fp));
}
} catch { /* not JSON */ }
return detectText(input, '<stdin>', resolve(null));
@@ -374,16 +379,10 @@ async function detectCli() {
}
for (const file of files) {
const ext = path.extname(file).toLowerCase();
// Each file resolves its own project design system (cached by root),
// so a scan spanning sibling projects applies the right rules per file.
const fileOptions = scanOptionsFor(file);
let fileFindings;
if (HTML_EXTENSIONS.has(ext)) {
fileFindings = await detectHtml(file, fileOptions);
} else {
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, fileOptions);
}
const fileFindings = await detectLocalFile(file, fileOptions);
// Annotate findings with import context
const importers = importedByMap.get(file);
if (importers && importers.size > 0) {
@@ -396,13 +395,8 @@ async function detectCli() {
}
} else if (stat.isFile()) {
if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;
const ext = path.extname(resolved).toLowerCase();
const fileOptions = scanOptionsFor(resolved);
if (HTML_EXTENSIONS.has(ext)) {
allFindings.push(...await detectHtml(resolved, fileOptions));
} else {
allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, fileOptions));
}
allFindings.push(...await detectLocalFile(resolved, fileOptions));
}
}
} finally {
+1
View File
@@ -107,6 +107,7 @@ export const SUITES = {
'tests/detect-antipatterns-fixtures.test.mjs',
'tests/detect-antipatterns-browser.test.mjs',
'tests/detect-cli-design-contamination.test.mjs',
'tests/detect-cli-stdin-dispatch.test.mjs',
],
},
],
+65
View File
@@ -0,0 +1,65 @@
import { after, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const cli = path.join(root, 'cli', 'bin', 'cli.js');
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-stdin-dispatch-'));
function detectStdinFile(filePath) {
const result = spawnSync(
process.execPath,
[cli, 'detect', '--json', '--no-config', '--no-design-system'],
{
input: JSON.stringify({ tool_input: { file_path: filePath } }),
encoding: 'utf8',
},
);
assert.equal(result.status, 2, result.stderr);
return JSON.parse(result.stdout);
}
after(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('detect CLI stdin file dispatch', () => {
it('uses the static HTML engine for an HTML tool-input path', () => {
const filePath = path.join(tempDir, 'page.html');
fs.writeFileSync(
path.join(tempDir, 'page.css'),
'.notice { border-left: 4px solid blue; border-radius: 12px; }',
);
fs.writeFileSync(filePath, `
<!doctype html>
<link rel="stylesheet" href="./page.css">
<div class="notice">Notice</div>
`);
const findings = detectStdinFile(filePath);
assert.ok(findings.some(
(item) => item.file === filePath && item.antipattern === 'side-tab',
));
});
it('uses the text engine for a non-HTML tool-input path', () => {
const filePath = path.join(tempDir, 'styles.css');
fs.writeFileSync(filePath, `
.grid {
background-image:
linear-gradient(#eee 1px, transparent 1px),
linear-gradient(90deg, #eee 1px, transparent 1px);
background-size: 24px 24px;
}
`);
const findings = detectStdinFile(filePath);
assert.ok(findings.some(
(item) => item.file === filePath && item.antipattern === 'codex-grid-background',
));
});
});