From ca121aa35f1119d03f239e9f11711282f83ea3d8 Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Wed, 8 Jul 2026 05:23:39 +0500 Subject: [PATCH] Fix: file-scoped wildcard ignores suppress non-value-bearing rules (#296) (#309) A file-scoped wildcard ignore (add-value "*" --file ) silently no-op'd for rules with no extractable value, such as side-tab. isIgnoredFindingValue bailed on an empty value before the wildcard/file-scope branch could run. Require a value only on the specific-value path; let the scoped wildcard match on rule + file. Mirrored in skill/scripts/hook-lib.mjs for CLI/hook parity. --- cli/lib/impeccable-config.mjs | 6 ++-- skill/scripts/hook-lib.mjs | 6 ++-- tests/cli-ignores.test.js | 43 +++++++++++++++++++++++++++++ tests/lib/impeccable-config.test.js | 12 ++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/cli/lib/impeccable-config.mjs b/cli/lib/impeccable-config.mjs index dc1c3d1c4..45c65e65a 100644 --- a/cli/lib/impeccable-config.mjs +++ b/cli/lib/impeccable-config.mjs @@ -459,11 +459,13 @@ export function filterDetectionFindings(findings, config) { function isIgnoredFindingValue(finding, ignoreValues) { if (!Array.isArray(ignoreValues) || ignoreValues.length === 0) return false; const rule = normalizeIgnoreRule(finding.antipattern); + if (!rule) return false; + // File-scoped wildcards suppress rules with no extractable value, such as side-tab. const value = extractFindingIgnoreValue(finding); - if (!rule || !value) return false; return ignoreValues.some((entry) => { + if (entry.rule !== rule) return false; const wildcardValue = entry.value === '*'; - if (entry.rule !== rule || (!wildcardValue && !ignoreValueMatches(rule, entry.value, value))) return false; + if (!wildcardValue && (!value || !ignoreValueMatches(rule, entry.value, value))) return false; if (!Array.isArray(entry.files) || entry.files.length === 0) return !wildcardValue; return findingMatchesScopedIgnoreFile(finding, entry.files); }); diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index b649211ec..f336356bb 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -711,11 +711,13 @@ export function filterFindings(findings, _content, _ext, config) { function isIgnoredFindingValue(finding, ignoreValues) { if (!Array.isArray(ignoreValues) || ignoreValues.length === 0) return false; const rule = normalizeIgnoreRule(finding.antipattern); + if (!rule) return false; + // File-scoped wildcards suppress rules with no extractable value, such as side-tab. const value = extractFindingIgnoreValue(finding); - if (!rule || !value) return false; return ignoreValues.some((entry) => { + if (entry.rule !== rule) return false; const wildcardValue = entry.value === '*'; - if (entry.rule !== rule || (!wildcardValue && !ignoreValueMatches(rule, entry.value, value))) return false; + if (!wildcardValue && (!value || !ignoreValueMatches(rule, entry.value, value))) return false; if (!Array.isArray(entry.files) || entry.files.length === 0) return !wildcardValue; return findingMatchesScopedIgnoreFile(finding, entry.files); }); diff --git a/tests/cli-ignores.test.js b/tests/cli-ignores.test.js index 697c77d22..95f676ed4 100644 --- a/tests/cli-ignores.test.js +++ b/tests/cli-ignores.test.js @@ -27,6 +27,16 @@ describe('impeccable ignores CLI', () => { return result; } + function detect(args, options = {}) { + const result = spawnSync(process.execPath, [CLI, 'detect', '--json', ...args], { + cwd: root, + encoding: 'utf-8', + ...options, + }); + if (result.error) throw result.error; + return result; + } + function readConfig(name = 'config.json') { return JSON.parse(readFileSync(join(root, '.impeccable', name), 'utf-8')); } @@ -65,6 +75,39 @@ describe('impeccable ignores CLI', () => { expect(raw.detector.ignoreValues).toEqual([]); }); + test('file-scoped wildcard value ignores suppress non-value-bearing rules only in matching files', () => { + mkdirSync(join(root, 'components'), { recursive: true }); + const triangle = [ + 'export function TopicCard() {', + ' return (', + '
', + ' );', + '}', + '', + ].join('\n'); + writeFileSync(join(root, 'components', 'TopicCard.jsx'), triangle); + writeFileSync(join(root, 'components', 'Other.jsx'), triangle.replace('TopicCard', 'Other')); + + const before = detect(['components/TopicCard.jsx']); + expect(before.status).toBe(2); + expect(before.stdout).toContain('side-tab'); + + expect(run(['add-value', 'side-tab', '*', '--file', '**/TopicCard.jsx']).status).toBe(0); + + const afterTarget = detect(['components/TopicCard.jsx']); + expect(afterTarget.status).toBe(0); + expect(afterTarget.stdout.trim()).toBe('[]'); + + const afterOther = detect(['components/Other.jsx']); + expect(afterOther.status).toBe(2); + expect(afterOther.stdout).toContain('side-tab'); + }); + test('rejects broad wildcard value ignores', () => { const result = run(['add-value', 'design-system-color', '*']); expect(result.status).not.toBe(0); diff --git a/tests/lib/impeccable-config.test.js b/tests/lib/impeccable-config.test.js index d55d6e96d..bdad2051c 100644 --- a/tests/lib/impeccable-config.test.js +++ b/tests/lib/impeccable-config.test.js @@ -199,6 +199,18 @@ describe('cli/lib/impeccable-config', () => { ]); }); + test('filterDetectionFindings honors file-scoped wildcard ignores for non-value-bearing rules', () => { + const findings = [ + { antipattern: 'side-tab', file: join(root, 'components', 'TopicCard.jsx'), line: 331, snippet: "borderLeft: '7px solid" }, + { antipattern: 'side-tab', file: join(root, 'components', 'Other.jsx'), line: 12, snippet: "borderLeft: '7px solid" }, + ]; + const filtered = filterDetectionFindings(findings, { + ignoreRules: [], + ignoreValues: [{ rule: 'side-tab', value: '*', files: ['**/TopicCard.jsx'] }], + }); + expect(filtered.map((f) => `${f.antipattern}:${f.line}`)).toEqual(['side-tab:12']); + }); + test('filterDetectionFindings matches equivalent design-system color values', () => { const findings = [ { antipattern: 'design-system-color', file: join(root, 'src', 'rgb.css'), line: 1, ignoreValue: 'rgb(139, 92, 246)' },