Skip to content

Commit 002e3b0

Browse files
authoredDec 31, 2024··
Added support for style selector parsing (#619)
1 parent fd076a4 commit 002e3b0

25 files changed

+2793
-7
lines changed
 

‎.changeset/tricky-melons-complain.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: added support for style selector parsing

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"eslint-visitor-keys": "^4.0.0",
6060
"espree": "^10.0.0",
6161
"postcss": "^8.4.49",
62-
"postcss-scss": "^4.0.9"
62+
"postcss-scss": "^4.0.9",
63+
"postcss-selector-parser": "^7.0.0"
6364
},
6465
"devDependencies": {
6566
"@changesets/changelog-github": "^0.5.0",

‎src/parser/index.ts

+25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { KEYS } from "../visitor-keys.js";
22
import { Context } from "../context/index.js";
33
import type {
44
Comment,
5+
SourceLocation,
56
SvelteProgram,
67
SvelteScriptElement,
78
SvelteStyleElement,
@@ -10,6 +11,11 @@ import type {
1011
import type { Program } from "estree";
1112
import type { ScopeManager } from "eslint-scope";
1213
import { Variable } from "eslint-scope";
14+
import type { Rule, Node } from "postcss";
15+
import type {
16+
Node as SelectorNode,
17+
Root as SelectorRoot,
18+
} from "postcss-selector-parser";
1319
import { parseScript, parseScriptInSvelte } from "./script.js";
1420
import type * as SvAST from "./svelte-ast-types.js";
1521
import type * as Compiler from "./svelte-ast-types-for-v5.js";
@@ -29,13 +35,15 @@ import {
2935
import { addReference } from "../scope/index.js";
3036
import {
3137
parseStyleContext,
38+
parseSelector,
3239
type StyleContext,
3340
type StyleContextNoStyleElement,
3441
type StyleContextParseError,
3542
type StyleContextSuccess,
3643
type StyleContextUnknownLang,
3744
styleNodeLoc,
3845
styleNodeRange,
46+
styleSelectorNodeLoc,
3947
} from "./style-context.js";
4048
import { getGlobalsForSvelte, getGlobalsForSvelteScript } from "./globals.js";
4149
import type { NormalizedParserOptions } from "./parser-options.js";
@@ -84,6 +92,12 @@ type ParseResult = {
8492
isSvelteScript: false;
8593
getSvelteHtmlAst: () => SvAST.Fragment | Compiler.Fragment;
8694
getStyleContext: () => StyleContext;
95+
getStyleSelectorAST: (rule: Rule) => SelectorRoot;
96+
styleNodeLoc: (node: Node) => Partial<SourceLocation>;
97+
styleNodeRange: (
98+
node: Node,
99+
) => [number | undefined, number | undefined];
100+
styleSelectorNodeLoc: (node: SelectorNode) => Partial<SourceLocation>;
87101
svelteParseContext: SvelteParseContext;
88102
}
89103
| {
@@ -221,6 +235,7 @@ function parseAsSvelte(
221235
(b): b is SvelteStyleElement => b.type === "SvelteStyleElement",
222236
);
223237
let styleContext: StyleContext | null = null;
238+
const selectorASTs: Map<Rule, SelectorRoot> = new Map();
224239

225240
resultScript.ast = ast as any;
226241
resultScript.services = Object.assign(resultScript.services || {}, {
@@ -235,8 +250,18 @@ function parseAsSvelte(
235250
}
236251
return styleContext;
237252
},
253+
getStyleSelectorAST(rule: Rule) {
254+
const cached = selectorASTs.get(rule);
255+
if (cached !== undefined) {
256+
return cached;
257+
}
258+
const ast = parseSelector(rule);
259+
selectorASTs.set(rule, ast);
260+
return ast;
261+
},
238262
styleNodeLoc,
239263
styleNodeRange,
264+
styleSelectorNodeLoc,
240265
svelteParseContext,
241266
});
242267
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);

‎src/parser/style-context.ts

+68-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { Node, Parser, Root } from "postcss";
1+
import type { Node, Parser, Root, Rule } from "postcss";
22
import postcss from "postcss";
33
import { parse as SCSSparse } from "postcss-scss";
4+
import {
5+
default as selectorParser,
6+
type Node as SelectorNode,
7+
type Root as SelectorRoot,
8+
} from "postcss-selector-parser";
49

510
import type { Context } from "../context/index.js";
611
import type { SourceLocation, SvelteStyleElement } from "../ast/index.js";
@@ -77,10 +82,25 @@ export function parseStyleContext(
7782
return { status: "parse-error", sourceLang, error };
7883
}
7984
fixPostCSSNodeLocation(sourceAst, styleElement);
80-
sourceAst.walk((node) => fixPostCSSNodeLocation(node, styleElement));
85+
sourceAst.walk((node) => {
86+
fixPostCSSNodeLocation(node, styleElement);
87+
});
8188
return { status: "success", sourceLang, sourceAst };
8289
}
8390

91+
/**
92+
* Parses a PostCSS Rule node's selector and returns its AST.
93+
*/
94+
export function parseSelector(rule: Rule): SelectorRoot {
95+
const processor = selectorParser();
96+
const root = processor.astSync(rule.selector);
97+
fixSelectorNodeLocation(root, rule);
98+
root.walk((node) => {
99+
fixSelectorNodeLocation(node, rule);
100+
});
101+
return root;
102+
}
103+
84104
/**
85105
* Extracts a node location (like that of any ESLint node) from a parsed svelte style node.
86106
*/
@@ -121,6 +141,24 @@ export function styleNodeRange(
121141
];
122142
}
123143

144+
/**
145+
* Extracts a node location (like that of any ESLint node) from a parsed svelte selector node.
146+
*/
147+
export function styleSelectorNodeLoc(
148+
node: SelectorNode,
149+
): Partial<SourceLocation> {
150+
return {
151+
start:
152+
node.source?.start !== undefined
153+
? {
154+
line: node.source.start.line,
155+
column: node.source.start.column - 1,
156+
}
157+
: undefined,
158+
end: node.source?.end,
159+
};
160+
}
161+
124162
/**
125163
* Fixes PostCSS AST locations to be relative to the whole file instead of relative to the <style> element.
126164
*/
@@ -144,3 +182,31 @@ function fixPostCSSNodeLocation(node: Node, styleElement: SvelteStyleElement) {
144182
node.source.end.column += styleElement.startTag.loc.end.column;
145183
}
146184
}
185+
186+
/**
187+
* Fixes selector AST locations to be relative to the whole file instead of relative to their parent rule.
188+
*/
189+
function fixSelectorNodeLocation(node: SelectorNode, rule: Rule) {
190+
if (node.source === undefined) {
191+
return;
192+
}
193+
const ruleLoc = styleNodeLoc(rule);
194+
195+
if (node.source.start !== undefined && ruleLoc.start !== undefined) {
196+
if (node.source.start.line === 1) {
197+
node.source.start.column += ruleLoc.start.column;
198+
}
199+
node.source.start.line += ruleLoc.start.line - 1;
200+
} else {
201+
node.source.start = undefined;
202+
}
203+
204+
if (node.source.end !== undefined && ruleLoc.start !== undefined) {
205+
if (node.source.end.line === 1) {
206+
node.source.end.column += ruleLoc.start.column;
207+
}
208+
node.source.end.line += ruleLoc.start.line - 1;
209+
} else {
210+
node.source.end = undefined;
211+
}
212+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
let a = 10
3+
</script>
4+
5+
<span class="myClass">Hello!</span>
6+
7+
<b>{a}</b>
8+
9+
<style>
10+
.myClass {
11+
color: red;
12+
}
13+
14+
b {
15+
font-size: xx-large;
16+
}
17+
18+
a:active,
19+
a::before,
20+
b + a,
21+
b + .myClass,
22+
a[data-key="value"] {
23+
color: blue;
24+
}
25+
</style>

0 commit comments

Comments
 (0)
Please sign in to comment.