Fix: file-scoped wildcard ignores suppress non-value-bearing rules (#296) (#309)

A file-scoped wildcard ignore (add-value <rule> "*" --file <glob>) 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.
This commit is contained in:
Abdul Wahab
2026-07-07 17:23:39 -07:00
committed by GitHub
parent a99bb976b7
commit ca121aa35f
4 changed files with 63 additions and 4 deletions
+4 -2
View File
@@ -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);
});
+4 -2
View File
@@ -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);
});
+43
View File
@@ -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 (',
' <div style={{',
' width: 0, height: 0,',
" borderLeft: '7px solid transparent',",
" borderRight: '7px solid transparent',",
" borderTop: '7px solid #fff',",
' }} />',
' );',
'}',
'',
].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);
+12
View File
@@ -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)' },