Skip to content

Commit 4e047b9

Browse files
skovhusclaude
andcommitted
fix(selector): normalize specificity hacks for :has() detection, bail on unresolvable media
Two review fixes: - Use specificity-normalized selector for isHasPattern so &&:has(${Icon}) is correctly detected as a :has() pattern instead of falling through to the descendant-component handler - Add state.markBail() in resolveMediaAndEmitComputedKeys when media query interpolation cannot resolve, preventing lossy transformation Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 6462600 commit 4e047b9

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

src/__tests__/transform.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5668,4 +5668,39 @@ export const App = () => (
56685668
// Should bail — compound :has + pseudo is not supported
56695669
expect(result.code).toBeNull();
56705670
});
5671+
5672+
it("should handle &:has(${Component}) with specificity hack (&&:has)", () => {
5673+
const source = `
5674+
import styled from "styled-components";
5675+
5676+
const Icon = styled.span\`
5677+
color: blue;
5678+
\`;
5679+
5680+
const Button = styled.button\`
5681+
background: lightgray;
5682+
5683+
&&:has(\${Icon}) {
5684+
background: lightyellow;
5685+
}
5686+
\`;
5687+
5688+
export const App = () => (
5689+
<div>
5690+
<Button>No icon</Button>
5691+
<Button>With icon <Icon>★</Icon></Button>
5692+
</div>
5693+
);
5694+
`;
5695+
5696+
const result = transformWithWarnings(
5697+
{ source, path: "test.tsx" },
5698+
{ jscodeshift: j, j, stats: () => {}, report: () => {} },
5699+
{ adapter: fixtureAdapter },
5700+
);
5701+
5702+
// Should transform (not bail) — && is a specificity hack normalized to &
5703+
expect(result.code).not.toBeNull();
5704+
expect(result.code).toContain("stylex.when.descendant");
5705+
});
56715706
});

src/internal/lower-rules/process-rules.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,9 @@ export function processDeclRules(ctx: DeclProcessingState): void {
413413
const isCssHelperPlaceholder = !!otherLocal && cssHelperNames.has(otherLocal);
414414

415415
const selTrim2 = rule.selector.trim();
416-
const isHasPattern = HAS_COMPONENT_SELECTOR_STRICT_RE.test(selTrim2);
416+
// Use specificity-normalized selector for :has() detection (e.g. &&:has → &:has)
417+
const normalizedSel2 = normalizeSpecificityHacks(selTrim2).normalized;
418+
const isHasPattern = HAS_COMPONENT_SELECTOR_STRICT_RE.test(normalizedSel2);
417419

418420
// `${Other}:pseudo &` (Icon reacting to ancestor hover/focus/etc.)
419421
// This is the inverse of `&:pseudo ${Child}` — the declaring component is the child,
@@ -2424,6 +2426,7 @@ function resolveMediaAndEmitComputedKeys(
24242426
},
24252427
);
24262428
if (resolved === null) {
2429+
state.markBail();
24272430
warnings.push({
24282431
severity: "warning",
24292432
type: "Unsupported: media query interpolation must be a simple imported reference (expressions like `value + 1` are not supported)",

0 commit comments

Comments
 (0)