mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
fix: stop treating the child combinator as a prelude boundary when pruning
removeSelectorAt walked backward to find the rule prelude and stopped at any '>', added so the walk would not escape past the <style> open tag. That same character is the CSS child combinator, so pruning one unused selector from a list like '.wrap > .orphan, .orphan' cut the prelude mid-list; when every remaining fragment equaled the flagged selector, the whole-rule branch then deleted from the cut point and left a dangling '.wrap >' in source. A '>' now bounds the walk only when it actually closes a <style ...> tag; combinators are walked through. Regression tests cover a mid-list combinator prune and the dangling- fragment shape (verified failing on the previous code). AI-assisted (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
20213a6817
commit
0c18cbc9ef
@@ -535,11 +535,20 @@ function removeSelectorAt(source, start, end) {
|
||||
if (braceIdx === -1) return { changed: false, selector, source };
|
||||
const bodyEnd = scanBlockEnd(source, braceIdx + 1);
|
||||
|
||||
// Prelude spans backward from the brace to the previous } ; { or style open.
|
||||
// Prelude spans backward from the brace to the previous } ; { or the end
|
||||
// of the <style> open tag. A bare `>` is NOT a boundary: it is the child
|
||||
// combinator, and cutting there truncates a selector list like
|
||||
// `.a > .b, .c` mid-prelude. Only a `>` that closes a `<style ...>` tag
|
||||
// bounds the walk.
|
||||
let preludeStart = start;
|
||||
for (let i = start - 1; i >= 0; i--) {
|
||||
const ch = source[i];
|
||||
if (ch === '}' || ch === '{' || ch === ';' || ch === '>') { preludeStart = i + 1; break; }
|
||||
if (ch === '}' || ch === '{' || ch === ';') { preludeStart = i + 1; break; }
|
||||
if (ch === '>') {
|
||||
const styleOpen = source.lastIndexOf('<style', i);
|
||||
if (styleOpen !== -1 && source.indexOf('>', styleOpen) === i) { preludeStart = i + 1; break; }
|
||||
continue; // child combinator inside the prelude
|
||||
}
|
||||
if (i === 0) preludeStart = 0;
|
||||
}
|
||||
const prelude = source.slice(preludeStart, braceIdx);
|
||||
|
||||
@@ -137,6 +137,33 @@ describe('compiler-driven pruning', () => {
|
||||
const { source } = pruneUnusedSelectors('<div>{broken', () => { throw new Error('nope'); });
|
||||
assert.equal(source, '<div>{broken');
|
||||
});
|
||||
|
||||
it('prunes past child combinators without truncating the selector list', () => {
|
||||
// The prelude walk must not treat the child combinator as a boundary:
|
||||
// cutting at the `>` of `.wrap > .item` used to rewrite the list from
|
||||
// mid-prelude.
|
||||
const component = `<div class="wrap"><p class="item">x</p></div>\n<style>\n .wrap > .item, .orphan { font-weight: bold; }\n .wrap { padding: 4px; }\n</style>`;
|
||||
const { source, removed } = pruneUnusedSelectors(component, compile);
|
||||
assert.deepEqual(removed, ['.orphan']);
|
||||
assert.match(source, /\.wrap > \.item \{ font-weight: bold; \}/);
|
||||
assert.match(source, /\.wrap \{ padding: 4px; \}/);
|
||||
const { warnings } = compile(source, { generate: false });
|
||||
assert.deepEqual(warnings.filter((w) => w.code === 'css_unused_selector'), []);
|
||||
});
|
||||
|
||||
it('removes a fully unused combinator rule without leaving a dangling fragment', () => {
|
||||
// The corruption shape: after a mid-prelude cut, every remaining fragment
|
||||
// equals the flagged selector, so the whole-rule branch deleted from the
|
||||
// cut point and left `.wrap >` dangling in source.
|
||||
const component = `<div class="wrap"><p class="item">x</p></div>\n<style>\n .wrap > .orphan, .orphan { color: blue; }\n .wrap { padding: 4px; }\n</style>`;
|
||||
const { source } = pruneUnusedSelectors(component, compile);
|
||||
assert.doesNotMatch(source, /\.orphan/);
|
||||
assert.doesNotMatch(source, /\.wrap >\s*\{/, 'no dangling combinator fragment');
|
||||
assert.doesNotMatch(source, /\.wrap >\s*$/m, 'no dangling combinator line');
|
||||
assert.match(source, /\.wrap \{ padding: 4px; \}/);
|
||||
const { warnings } = compile(source, { generate: false });
|
||||
assert.deepEqual(warnings.filter((w) => w.code === 'css_unused_selector'), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('postcondition scanner', () => {
|
||||
|
||||
Reference in New Issue
Block a user