Files
pbakaus_impeccable/cli/bin/commands/ignores.mjs
T
be87f5eb86 Fix: refuse inert exact ignore-value entries (#662)
ignore-value stored exact values for rules that cannot extract one, so the entries never matched. Refuse them and point at "*" --file.

AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:43:36 +05:00

356 lines
14 KiB
JavaScript

import path from 'node:path';
import {
getConfigPath,
getLocalConfigPath,
normalizeIgnoreValue,
readDetectionConfig,
readRawDetectionConfig,
writeDetectionConfig,
extractFindingIgnoreValue,
} from '../../lib/impeccable-config.mjs';
const ACTION_ALIASES = new Map([
['status', 'list'],
['ls', 'list'],
['list', 'list'],
['add-rule', 'add-rule'],
['ignore-rule', 'add-rule'],
['add-file', 'add-file'],
['ignore-file', 'add-file'],
['add-value', 'add-value'],
['ignore-value', 'add-value'],
['update-value', 'add-value'],
['remove-rule', 'remove-rule'],
['rm-rule', 'remove-rule'],
['remove-file', 'remove-file'],
['rm-file', 'remove-file'],
['remove-value', 'remove-value'],
['rm-value', 'remove-value'],
['clear', 'clear'],
]);
function printUsage() {
console.log(`Usage: impeccable ignores <action> [options]
Manage detector ignores in .impeccable config.
Actions:
list Show merged, shared, and local ignores
add-rule <rule> [--all-values] Ignore a rule
add-file <glob> Ignore files by glob
add-value <rule> <value> Ignore one rule/value pair
remove-rule <rule> Remove a rule ignore
remove-file <glob> Remove a file ignore
remove-value <rule> <value> Remove a rule/value ignore
clear Clear detector ignores in the selected scope
Scope:
--shared Write .impeccable/config.json (default)
--local Write .impeccable/config.local.json
--all For remove/clear, apply to shared and local
Value options:
--file <glob> Scope add-value/remove-value to a file glob
--reason <text> Store or update a reason on add-value
Examples:
impeccable ignores add-file "src/legacy/**"
impeccable ignores add-value overused-font Inter --reason "Brand font"
impeccable ignores add-value design-system-color "*" --file "src/demo.css"
impeccable ignores remove-value overused-font Inter`);
}
function parseScope(args, { allowAll = false } = {}) {
const rest = [];
let local = false;
let shared = false;
let all = false;
for (const arg of args) {
if (arg === '--local') local = true;
else if (arg === '--shared') shared = true;
else if (arg === '--all') all = true;
else rest.push(arg);
}
if ([local, shared, all].filter(Boolean).length > 1) {
throw new Error(`Pass only one scope flag: --shared${allowAll ? ', --local, or --all' : ' or --local'}`);
}
if (all && !allowAll) throw new Error('--all is only supported for remove and clear actions');
return { local, all, rest };
}
// An empty glob used to be dropped by filter(Boolean), so `--file=` reported
// success and wrote an entry with no files: the user asked to scope a rule to one
// file and silently got the project-wide suppression instead. Refuse it.
function requireGlob(raw, flag) {
const glob = String(raw ?? '').trim();
if (!glob) throw new Error(`${flag} requires a non-empty glob`);
// A following flag is not a glob. `--file --reason "why"` consumed `--reason`
// as the scope and left the reason text to fold into the value, storing
// value="* why" files=["--reason"] and reporting success. Same silent-no-op
// class as an unknown flag folding into the value; refuse it the same way.
if (glob.startsWith('--')) throw new Error(`${flag} requires a glob, got the flag ${glob}`);
return glob;
}
function parseValueArgs(args, { allowUnscopedWildcard = false } = {}) {
const positionals = [];
const files = [];
let reason = '';
for (let i = 0; i < args.length; i++) {
const arg = String(args[i] || '');
if (arg === '--reason') {
const chunks = [];
while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) chunks.push(args[++i]);
reason = chunks.join(' ').trim();
} else if (arg.startsWith('--reason=')) {
reason = arg.slice('--reason='.length).trim();
} else if (arg === '--file' || arg === '--files') {
if (i + 1 >= args.length) throw new Error(`${arg} requires a glob`);
files.push(requireGlob(args[++i], arg));
} else if (arg.startsWith('--file=')) {
files.push(requireGlob(arg.slice('--file='.length), '--file'));
} else if (arg.startsWith('--files=')) {
files.push(requireGlob(arg.slice('--files='.length), '--files'));
} else if (arg.startsWith('--')) {
throw new Error(`Unknown add-value flag: ${arg}`);
} else {
positionals.push(arg);
}
}
const [rule, ...valueParts] = positionals;
const value = normalizeIgnoreValue(valueParts.join(' '));
if (!rule || !value) throw new Error('Pass a rule id and value, e.g. impeccable ignores add-value overused-font Inter');
// Sorted: the dedup key compares the files array, so an unsorted scope made
// `--file b.css --file a.css` a different entry from `--file a.css --file b.css`.
const scopedFiles = Array.from(new Set(files.filter(Boolean))).sort();
if (value === '*' && scopedFiles.length === 0 && !allowUnscopedWildcard) {
throw new Error('Wildcard value ignores must be scoped with --file <glob>.');
}
return {
rule: String(rule).trim().toLowerCase(),
value,
files: scopedFiles,
reason,
};
}
function formatValues(values) {
if (!values.length) return '(none)';
return values
.map((entry) => {
const fileSuffix = Array.isArray(entry.files) && entry.files.length
? ` [${entry.files.join(', ')}]`
: '';
const reasonSuffix = entry.reason ? ` - ${entry.reason}` : '';
return `${entry.rule}=${entry.value}${fileSuffix}${reasonSuffix}`;
})
.join(', ');
}
function formatConfig(label, config) {
return [
`${label}:`,
` ignoreRules: ${config.ignoreRules.length ? config.ignoreRules.join(', ') : '(none)'}`,
` ignoreFiles: ${config.ignoreFiles.length ? config.ignoreFiles.join(', ') : '(none)'}`,
` ignoreValues: ${formatValues(config.ignoreValues)}`,
` designSystem: ${config.designSystem?.enabled === false ? 'disabled' : 'enabled'}`,
].join('\n');
}
function list(cwd) {
const merged = readDetectionConfig(cwd);
const shared = readRawDetectionConfig(cwd);
const local = readRawDetectionConfig(cwd, { local: true });
return [
'Impeccable detector ignores',
` shared file: ${path.relative(cwd, getConfigPath(cwd)) || getConfigPath(cwd)}`,
` local file: ${path.relative(cwd, getLocalConfigPath(cwd)) || getLocalConfigPath(cwd)}`,
'',
formatConfig('Merged', merged),
'',
formatConfig('Shared', shared),
'',
formatConfig('Local', local),
].join('\n');
}
function readScopeConfig(cwd, local) {
return readRawDetectionConfig(cwd, { local });
}
function writeScopeConfig(cwd, config, local) {
return writeDetectionConfig(cwd, config, { local });
}
function parseRuleArgs(args) {
const positionals = [];
let allValues = false;
for (let i = 0; i < args.length; i++) {
const arg = String(args[i] || '');
if (arg === '--all-values') {
allValues = true;
} else if (arg === '--reason') {
while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) i++;
} else if (arg.startsWith('--reason=')) {
// Accepted for symmetry with add-value; ignoreRules stores ids only.
} else if (arg.startsWith('--')) {
throw new Error(`Unknown add-rule flag: ${arg}`);
} else {
positionals.push(arg);
}
}
return {
rule: String(positionals[0] || '').trim().toLowerCase(),
allValues,
};
}
function addRule(cwd, args) {
const { local, rest } = parseScope(args);
const { rule, allValues } = parseRuleArgs(rest);
if (!rule) throw new Error('Pass a rule id, e.g. impeccable ignores add-rule side-tab');
if (rule === 'overused-font' && !allValues) {
throw new Error('overused-font is value-specific by default. Use add-value overused-font <font>, or add-rule overused-font --all-values for broad suppression.');
}
const config = readScopeConfig(cwd, local);
if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
const target = writeScopeConfig(cwd, config, local);
return `Added ${rule} to ${local ? 'local' : 'shared'} detector ignoreRules (${path.relative(cwd, target) || target}).`;
}
function addFile(cwd, args) {
const { local, rest } = parseScope(args);
const glob = String(rest[0] || '').trim();
if (!glob) throw new Error('Pass a glob, e.g. impeccable ignores add-file "src/legacy/**"');
const config = readScopeConfig(cwd, local);
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
const target = writeScopeConfig(cwd, config, local);
return `Added ${glob} to ${local ? 'local' : 'shared'} detector ignoreFiles (${path.relative(cwd, target) || target}).`;
}
function addValue(cwd, args) {
const { local, rest } = parseScope(args);
const parsed = parseValueArgs(rest);
if (parsed.value !== '*' && !extractFindingIgnoreValue({ antipattern: parsed.rule, ignoreValue: parsed.value })) {
throw new Error(`${parsed.rule} has no extractable ignore value. Use impeccable ignores add-value ${parsed.rule} "*" --file <glob> to suppress it in matching files.`);
}
const config = readScopeConfig(cwd, local);
const key = ignoreValueKey(parsed);
const existing = config.ignoreValues.find((entry) => ignoreValueKey(entry) === key);
if (existing) {
if (parsed.reason) existing.reason = parsed.reason;
if (parsed.files.length) existing.files = parsed.files;
} else {
// rule, value, files, createdAt, reason — the same order the normalizers emit,
// so a fresh entry survives the next write untouched.
const entry = {
rule: parsed.rule,
value: parsed.value,
};
if (parsed.files.length) entry.files = parsed.files;
entry.createdAt = new Date().toISOString();
if (parsed.reason) entry.reason = parsed.reason;
config.ignoreValues.push(entry);
}
const target = writeScopeConfig(cwd, config, local);
return `Added ${parsed.rule}=${parsed.value} to ${local ? 'local' : 'shared'} detector ignoreValues (${path.relative(cwd, target) || target}).`;
}
function removeFromScopes(cwd, args, remover) {
const { local, all, rest } = parseScope(args, { allowAll: true });
const scopes = all ? [false, true] : [local];
const removed = [];
for (const isLocal of scopes) {
const config = readScopeConfig(cwd, isLocal);
const count = remover(config, rest);
if (count > 0) {
const target = writeScopeConfig(cwd, config, isLocal);
removed.push(`${count} from ${isLocal ? 'local' : 'shared'} (${path.relative(cwd, target) || target})`);
}
}
return removed.length ? `Removed ${removed.join(', ')}.` : 'No matching detector ignore found.';
}
function removeRule(cwd, args) {
return removeFromScopes(cwd, args, (config, rest) => {
const rule = String(rest[0] || '').trim().toLowerCase();
if (!rule) throw new Error('Pass a rule id, e.g. impeccable ignores remove-rule side-tab');
const before = config.ignoreRules.length;
config.ignoreRules = config.ignoreRules.filter((entry) => entry !== rule);
return before - config.ignoreRules.length;
});
}
function removeFile(cwd, args) {
return removeFromScopes(cwd, args, (config, rest) => {
const glob = String(rest[0] || '').trim();
if (!glob) throw new Error('Pass a glob, e.g. impeccable ignores remove-file "src/legacy/**"');
const before = config.ignoreFiles.length;
config.ignoreFiles = config.ignoreFiles.filter((entry) => entry !== glob);
return before - config.ignoreFiles.length;
});
}
function removeValue(cwd, args) {
return removeFromScopes(cwd, args, (config, rest) => {
const parsed = parseValueArgs(rest, { allowUnscopedWildcard: true });
const key = ignoreValueKey(parsed);
const before = config.ignoreValues.length;
config.ignoreValues = config.ignoreValues.filter((entry) => ignoreValueKey(entry) !== key);
return before - config.ignoreValues.length;
});
}
function clear(cwd, args) {
const { local, all, rest } = parseScope(args, { allowAll: true });
if (rest.length > 0) throw new Error('clear does not take positional arguments');
const scopes = all ? [false, true] : [local];
for (const isLocal of scopes) {
const config = readScopeConfig(cwd, isLocal);
config.ignoreRules = [];
config.ignoreFiles = [];
config.ignoreValues = [];
writeScopeConfig(cwd, config, isLocal);
}
return `Cleared detector ignores in ${all ? 'shared and local config' : local ? 'local config' : 'shared config'}.`;
}
function ignoreValueKey(entry) {
// Sorted: a file scope is a set. Comparing stored order made an on-disk scope
// miss the sorted argv form, so a re-add duplicated the entry and a remove
// silently failed. Every key that hashes `files` must sort — there are four.
const files = Array.isArray(entry.files) && entry.files.length ? [...entry.files].sort().join('\x1f') : '';
return `${String(entry.rule || '').trim().toLowerCase()}\0${normalizeIgnoreValue(entry.value)}\0${files}`;
}
export async function run(args = [], opts = {}) {
const cwd = opts.cwd || process.cwd();
const actionArg = args[0] || 'list';
if (actionArg === '--help' || actionArg === '-h') {
printUsage();
return;
}
const action = ACTION_ALIASES.get(String(actionArg).toLowerCase());
if (!action) {
throw new Error(`Unknown ignores action: ${actionArg}. Run "impeccable ignores --help".`);
}
const rest = args.slice(1);
let out;
switch (action) {
case 'list': out = list(cwd); break;
case 'add-rule': out = addRule(cwd, rest); break;
case 'add-file': out = addFile(cwd, rest); break;
case 'add-value': out = addValue(cwd, rest); break;
case 'remove-rule': out = removeRule(cwd, rest); break;
case 'remove-file': out = removeFile(cwd, rest); break;
case 'remove-value': out = removeValue(cwd, rest); break;
case 'clear': out = clear(cwd, rest); break;
}
if (out) console.log(out);
}