Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/detector/shared/inline-ignores.mjs
1/**2* Inline, in-file ignore directives — eslint-disable-style waivers that live at3* the point they apply and travel with the artifact instead of (or alongside)4* an ignore in `.impeccable/config.json`.5*6* A config ignore is the right default for repo-wide policy. This complements it7* for the one case config can't cover: a waiver that belongs to a single file and8* needs to follow that file when it leaves the repo — a generated/exported9* standalone document, an emailed HTML file, a snippet scanned out of context.10*11* Comment-syntax-agnostic: the directive is a raw token matched anywhere on a12* line, so the same marker works across every comment style impeccable scans —13* `//`, `/* *\/`, `<!-- -->`, `#`, `{/* *\/}`, `{# #}`. Trailing comment closers14* are stripped before the rule list is parsed.15*16* Syntax (reason optional; eslint `--` or biome `:` separator):17*18* impeccable-disable <rule>[, <rule>...] [-- reason] whole file19* impeccable-disable-line <rule>... [-- reason] the same line20* impeccable-disable-next-line <rule>... [-- reason] the following line21* impeccable-disable bare / `*` = every rule22*23* Examples:24*25* <!-- impeccable-disable overused-font -- exported brand doc, font is first-party -->26* .brand { font-family: Inter; } /* impeccable-disable-line overused-font *\/27* // impeccable-disable-next-line bounce-easing: intentional playful affordance28*29* Behavior is suppression, for parity with config ignores: a matched directive30* drops the finding. The inline reason is self-documenting in the diff; it is not31* required and is discarded at scan time (only used here to keep reason words out32* of the parsed rule list).33*/3435const DIRECTIVE_RE = /impeccable-(disable-next-line|disable-line|disable)\b[ \t]*([^\n\r]*)/gi;3637// Trailing comment closers, so `*/`, `*/}`, `-->`, `*}`, `#}`, `%>`, `}}` don't38// leak into the rule list. Anchored to end-of-line; the leading `\s*` mops up the39// space before the closer. `--+>` covers `-->` and any longer dash run.40const TRAILING_CLOSER_RE = /\s*(?:\*\/\}?|--+>|\*\}|#\}|%>|\}\})\s*$/;4142function normalizeRule(token) {43return String(token || '').trim().toLowerCase();44}4546// Split the directive remainder into rule tokens, dropping any human reason that47// follows an eslint-style `--` or biome-style `:` separator. Rule ids only ever48// contain single hyphens (`overused-font`, `bounce-easing`), so `--` and `:`49// are unambiguous separators.50function parseRuleList(remainder) {51let text = String(remainder || '').replace(TRAILING_CLOSER_RE, '').trim();52// Cut off a human reason at the first `--` (eslint) or `:` (biome) separator.53const reasonSep = text.match(/\s*(?:--+|:)\s*/);54if (reasonSep) text = text.slice(0, reasonSep.index);55const tokens = text.split(/[\s,]+/).map(normalizeRule).filter(Boolean);56if (tokens.length === 0 || tokens.includes('*')) return ['*'];57return tokens;58}5960function addRules(set, rules) {61for (const rule of rules) set.add(rule);62}6364function getSet(map, key) {65let set = map.get(key);66if (!set) {67set = new Set();68map.set(key, set);69}70return set;71}7273/**74* Parse every inline ignore directive in a file's raw text.75*76* Returns sets keyed by the 1-based line the directive *targets* so matching is a77* direct lookup:78* - file: rules disabled for the whole file79* - line: line -> rules disabled on that exact line (disable-line)80* - nextLine: line -> rules disabled on that line (disable-next-line on line-1)81*82* `*` in any set means "every rule".83*/84function parseInlineIgnores(content) {85const result = { file: new Set(), line: new Map(), nextLine: new Map() };86const text = typeof content === 'string' ? content : '';87// Cheap bail-out: the substring must be present for any directive to exist.88// Case-insensitive to match DIRECTIVE_RE's `i` flag (e.g. `Impeccable-Disable`).89if (!/impeccable-disable/i.test(text)) return result;9091// Split on `\n` only, exactly as detectText numbers lines, so directive line92// keys line up with finding `line` values (incl. on `\r`-only line endings).93// The directive regex excludes `\r`, so a trailing `\r` on `\r\n` files is94// never captured into the rule list.95const lines = text.split('\n');96for (let i = 0; i < lines.length; i++) {97DIRECTIVE_RE.lastIndex = 0;98let m;99while ((m = DIRECTIVE_RE.exec(lines[i])) !== null) {100const variant = m[1].toLowerCase();101const rules = parseRuleList(m[2]);102if (variant === 'disable') {103addRules(result.file, rules);104} else if (variant === 'disable-line') {105addRules(getSet(result.line, i + 1), rules);106} else {107// disable-next-line on line i+1 targets line i+2.108addRules(getSet(result.nextLine, i + 2), rules);109}110}111}112return result;113}114115function setMatches(set, rule) {116return Boolean(set) && (set.has('*') || set.has(rule));117}118119function isInlineIgnored(finding, directives) {120const rule = normalizeRule(finding && finding.antipattern);121if (!rule) return false;122if (setMatches(directives.file, rule)) return true;123const line = Number(finding && finding.line) || 0;124if (line > 0) {125if (setMatches(directives.line.get(line), rule)) return true;126if (setMatches(directives.nextLine.get(line), rule)) return true;127}128return false;129}130131function hasDirectives(directives) {132return directives.file.size > 0 || directives.line.size > 0 || directives.nextLine.size > 0;133}134135/**136* Drop findings waived by an inline directive in the same file's source text.137* Findings without a usable line number (e.g. static-HTML page-level findings)138* are only matched by whole-file directives — which is the standalone-document139* case this primitive exists for.140*/141function applyInlineIgnores(findings, content) {142if (!Array.isArray(findings) || findings.length === 0) return findings;143const directives = parseInlineIgnores(content);144if (!hasDirectives(directives)) return findings;145return findings.filter((finding) => !isInlineIgnored(finding, directives));146}147148export { parseInlineIgnores, applyInlineIgnores, isInlineIgnored };149