Skip to content

Commit df48d58

Browse files
cursoragentskovhus
andcommitted
fix: point selector warning LOCs at the actual selector line
findSelectorLineOffset only handled descendant (`& `) and child (`& > `) combinator prefixes when stripping Stylis-normalized selectors to search for them in the raw CSS. Sibling combinators (`& +`, `& ~`) were not handled, so warnings for selectors like `& + span` or `& ~ div` always pointed at the template literal start (line offset 0) instead of the selector's actual line. Extend the combinator strip regex from `^&\s*>\s*` to `^&\s*[>+~]\s*` and broaden the search prefix to also match combinator characters preceding the tag name in the raw CSS. Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
1 parent 84c0da8 commit df48d58

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

src/__tests__/css-ir.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,42 @@ describe("findSelectorLineOffset", () => {
193193
}`;
194194
expect(findSelectorLineOffset(css, "& > button")).toBe(2);
195195
});
196+
197+
it("finds adjacent sibling selectors (Stylis '&+span' → raw '+ span {')", () => {
198+
const css = `color: red;
199+
200+
& + span {
201+
margin-left: 8px;
202+
}`;
203+
expect(findSelectorLineOffset(css, "&+span")).toBe(2);
204+
});
205+
206+
it("finds general sibling selectors (Stylis '&~div' → raw '~ div {')", () => {
207+
const css = `color: red;
208+
209+
& ~ div {
210+
opacity: 0.5;
211+
}`;
212+
expect(findSelectorLineOffset(css, "&~div")).toBe(2);
213+
});
214+
215+
it("finds sibling selectors written without & (Stylis '&+span' → raw '+ span {')", () => {
216+
const css = `color: red;
217+
218+
+ span {
219+
margin-left: 8px;
220+
}`;
221+
expect(findSelectorLineOffset(css, "&+span")).toBe(2);
222+
});
223+
224+
it("finds child combinator with Stylis-normalized spaces (Stylis '&>button' → raw '> button {')", () => {
225+
const css = `color: red;
226+
227+
> button {
228+
opacity: 0.5;
229+
}`;
230+
expect(findSelectorLineOffset(css, "&>button")).toBe(2);
231+
});
196232
});
197233

198234
/** Helper to create a minimal slot for testing (expression is unused by the IR). */

src/__tests__/transform.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,66 @@ export const App = () => <Container />;
607607
expect(typeof warning.loc?.column).toBe("number");
608608
});
609609

610+
it("should warn with correct line number for sibling combinator selector", () => {
611+
const source = `
612+
import styled from 'styled-components';
613+
614+
const Box = styled.div\`
615+
color: red;
616+
617+
& + span {
618+
margin-left: 8px;
619+
}
620+
\`;
621+
622+
export const App = () => <Box />;
623+
`;
624+
625+
const result = transformWithWarnings(
626+
{ source, path: "test.tsx" },
627+
{ jscodeshift: j, j, stats: () => {}, report: () => {} },
628+
{ adapter: fixtureAdapter },
629+
);
630+
631+
expect(result.code).toBeNull();
632+
const warning = result.warnings.find(
633+
(w) => w.type === "Unsupported selector: sibling combinator",
634+
);
635+
expect(warning).toBeDefined();
636+
// Line 4 is template start, `& + span` is on line 7 (3 lines into template content)
637+
expect(warning?.loc?.line).toBe(7);
638+
});
639+
640+
it("should warn with correct line number for descendant/child/sibling selector", () => {
641+
const source = `
642+
import styled from 'styled-components';
643+
644+
const Box = styled.div\`
645+
color: red;
646+
647+
a {
648+
text-decoration: none;
649+
}
650+
\`;
651+
652+
export const App = () => <Box><span /></Box>;
653+
`;
654+
655+
const result = transformWithWarnings(
656+
{ source, path: "test.tsx" },
657+
{ jscodeshift: j, j, stats: () => {}, report: () => {} },
658+
{ adapter: fixtureAdapter },
659+
);
660+
661+
expect(result.code).toBeNull();
662+
const warning = result.warnings.find(
663+
(w) => w.type === "Unsupported selector: descendant/child/sibling selector",
664+
);
665+
expect(warning).toBeDefined();
666+
// Line 4 is template start, `a {` is on line 7 (3 lines into template content)
667+
expect(warning?.loc?.line).toBe(7);
668+
});
669+
610670
it("should emit info warning for & + & (adjacent sibling broadens to general)", () => {
611671
const source = `
612672
import styled from "styled-components";

src/internal/css-ir.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -634,11 +634,15 @@ export function findSelectorLineOffset(rawCss: string, selector: string): number
634634
}
635635
}
636636

637-
// For element selectors: Stylis produces "& svg" from "svg { ... }".
638-
// Strip the "& " prefix and search for the tag name followed by whitespace or {.
639-
const stripped = selector.replace(/^&\s+/, "").replace(/^&\s*>\s*/, "");
637+
// For element/combinator selectors: Stylis produces "& svg" from "svg { ... }",
638+
// "&>button" from "> button { ... }", "&+span" from "+ span { ... }", etc.
639+
// Strip the "& " prefix and any combinator (>, +, ~), then search for the
640+
// remaining text followed by whitespace or {.
641+
const stripped = selector.replace(/^&\s+/, "").replace(/^&\s*[>+~]\s*/, "");
640642
if (stripped !== selector) {
641-
const tagMatch = rawCss.match(new RegExp(`(?:^|\\s)${escapeRegExp(stripped)}\\s*[{,]`, "m"));
643+
const tagMatch = rawCss.match(
644+
new RegExp(`(?:^|[\\s>+~])${escapeRegExp(stripped)}\\s*[{,]`, "m"),
645+
);
642646
if (tagMatch) {
643647
return countNewlinesBefore(rawCss, tagMatch.index! + 1);
644648
}

0 commit comments

Comments
 (0)