diff --git a/cli/engine/cli/main.mjs b/cli/engine/cli/main.mjs index 9f0b67105..4476d04f1 100644 --- a/cli/engine/cli/main.mjs +++ b/cli/engine/cli/main.mjs @@ -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, '', 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 { diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index 7f726fa6e..4fba4df6d 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -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', ], }, ], diff --git a/tests/detect-cli-stdin-dispatch.test.mjs b/tests/detect-cli-stdin-dispatch.test.mjs new file mode 100644 index 000000000..2481d00f8 --- /dev/null +++ b/tests/detect-cli-stdin-dispatch.test.mjs @@ -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, ` + + +
Notice
+ `); + + 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', + )); + }); +});