Skip to content

Commit d170f91

Browse files
marekdedicota-meshi
andauthoredJun 10, 2023
Added style AST to parser services (#340)
* Added postcss and postcss-scss as dependencies * Parsing style AST as part of parser services * Added a JSDoc for parseStyleAst, removed commented out code * Added a changeset for PostCSS AST with parser services * Wrapped style AST in StyleContext * Logging postcss parse error to StyleContext * Moved style constext parsing to own file * Fixed PostCSS node locations * Not logging PostCSS errors for now * Using a discriminated union for StyleContext * Including parsing errors in StyleContext * Added infrastructure for style-context tests * Added a test with self-closing style element * Not printing file name in style-context test fixtures * Passing style element to fixPostCSSNodeLocation * Fixed column offset on style root node * Added a PostCSS AST test with simple CSS * Added a PostCSS AST test with empty <style> element * Fixed column offset in one-line styles * Added a test for one-line style * Ignoring another possible filename location in style-context tests * Added tests with invalid style * Added tests with unknown style lang * Added a test for parsing SCSS * Added a test with unrelated style attribute * ESLint fix * Added a converter functions for ESTree-style range and location for PostCSS nodes * Fixed incorrect Range conversion * Added tests for converter functions for ESTree-style range and location for PostCSS nodes * Exposing the StyleContext interface * Exposing styleNodeLoc and styleNodeRange functions as part of the parser services --------- Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
1 parent 2437a81 commit d170f91

29 files changed

+1204
-4
lines changed
 

‎.changeset/short-ducks-attend.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
added PostCSS AST of styles to parser services

‎package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
"dependencies": {
5656
"eslint-scope": "^7.0.0",
5757
"eslint-visitor-keys": "^3.0.0",
58-
"espree": "^9.0.0"
58+
"espree": "^9.0.0",
59+
"postcss": "^8.4.23",
60+
"postcss-scss": "^4.0.6"
5961
},
6062
"devDependencies": {
6163
"@changesets/changelog-github": "^0.4.8",

‎src/index.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import { parseForESLint } from "./parser";
21
import * as AST from "./ast";
32
import { traverseNodes } from "./traverse";
43
import { KEYS } from "./visitor-keys";
54
import { ParseError } from "./errors";
5+
export {
6+
parseForESLint,
7+
StyleContext,
8+
StyleContextNoStyleElement,
9+
StyleContextParseError,
10+
StyleContextSuccess,
11+
StyleContextUnknownLang,
12+
} from "./parser";
613
export * as meta from "./meta";
714
export { name } from "./meta";
815

916
export { AST, ParseError };
1017

11-
// parser
12-
export { parseForESLint };
1318
// Keys
1419
// eslint-disable-next-line @typescript-eslint/naming-convention -- ignore
1520
export const VisitorKeys = KEYS;

‎src/parser/index.ts

+30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
Comment,
55
SvelteProgram,
66
SvelteScriptElement,
7+
SvelteStyleElement,
78
Token,
89
} from "../ast";
910
import type { Program } from "estree";
@@ -21,6 +22,24 @@ import {
2122
import { ParseError } from "../errors";
2223
import { parseTypeScript } from "./typescript";
2324
import { addReference } from "../scope";
25+
import {
26+
parseStyleContext,
27+
type StyleContext,
28+
type StyleContextNoStyleElement,
29+
type StyleContextParseError,
30+
type StyleContextSuccess,
31+
type StyleContextUnknownLang,
32+
styleNodeLoc,
33+
styleNodeRange,
34+
} from "./style-context";
35+
36+
export {
37+
StyleContext,
38+
StyleContextNoStyleElement,
39+
StyleContextParseError,
40+
StyleContextSuccess,
41+
StyleContextUnknownLang,
42+
};
2443

2544
export interface ESLintProgram extends Program {
2645
comments: Comment[];
@@ -50,6 +69,7 @@ export function parseForESLint(
5069
services: Record<string, any> & {
5170
isSvelte: true;
5271
getSvelteHtmlAst: () => SvAST.Fragment;
72+
getStyleContext: () => StyleContext;
5373
};
5474
visitorKeys: { [type: string]: string[] };
5575
scopeManager: ScopeManager;
@@ -166,12 +186,22 @@ export function parseForESLint(
166186
);
167187
}
168188

189+
const styleElement = ast.body.find(
190+
(b): b is SvelteStyleElement => b.type === "SvelteStyleElement"
191+
);
192+
const styleContext = parseStyleContext(styleElement, ctx);
193+
169194
resultScript.ast = ast as any;
170195
resultScript.services = Object.assign(resultScript.services || {}, {
171196
isSvelte: true,
172197
getSvelteHtmlAst() {
173198
return resultTemplate.svelteAst.html;
174199
},
200+
getStyleContext() {
201+
return styleContext;
202+
},
203+
styleNodeLoc,
204+
styleNodeRange,
175205
});
176206
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);
177207

‎src/parser/style-context.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import type { Node, Parser, Root } from "postcss";
2+
import postcss from "postcss";
3+
import { parse as SCSSparse } from "postcss-scss";
4+
5+
import type { Context } from "../context";
6+
import type { SourceLocation, SvelteStyleElement } from "../ast";
7+
8+
export type StyleContext =
9+
| StyleContextNoStyleElement
10+
| StyleContextParseError
11+
| StyleContextSuccess
12+
| StyleContextUnknownLang;
13+
14+
export interface StyleContextNoStyleElement {
15+
status: "no-style-element";
16+
}
17+
18+
export interface StyleContextParseError {
19+
status: "parse-error";
20+
sourceLang: string;
21+
error: any;
22+
}
23+
24+
export interface StyleContextSuccess {
25+
status: "success";
26+
sourceLang: string;
27+
sourceAst: Root;
28+
}
29+
30+
export interface StyleContextUnknownLang {
31+
status: "unknown-lang";
32+
sourceLang: string;
33+
}
34+
35+
/**
36+
* Extracts style source from a SvelteStyleElement and parses it into a PostCSS AST.
37+
*/
38+
export function parseStyleContext(
39+
styleElement: SvelteStyleElement | undefined,
40+
ctx: Context
41+
): StyleContext {
42+
if (!styleElement || !styleElement.endTag) {
43+
return { status: "no-style-element" };
44+
}
45+
let sourceLang = "css";
46+
for (const attribute of styleElement.startTag.attributes) {
47+
if (
48+
attribute.type === "SvelteAttribute" &&
49+
attribute.key.name === "lang" &&
50+
attribute.value.length > 0 &&
51+
attribute.value[0].type === "SvelteLiteral"
52+
) {
53+
sourceLang = attribute.value[0].value;
54+
}
55+
}
56+
let parseFn: Parser<Root>, sourceAst: Root;
57+
switch (sourceLang) {
58+
case "css":
59+
parseFn = postcss.parse;
60+
break;
61+
case "scss":
62+
parseFn = SCSSparse;
63+
break;
64+
default:
65+
return { status: "unknown-lang", sourceLang };
66+
}
67+
const styleCode = ctx.code.slice(
68+
styleElement.startTag.range[1],
69+
styleElement.endTag.range[0]
70+
);
71+
try {
72+
sourceAst = parseFn(styleCode, {
73+
from: ctx.parserOptions.filePath,
74+
});
75+
} catch (error) {
76+
return { status: "parse-error", sourceLang, error };
77+
}
78+
fixPostCSSNodeLocation(sourceAst, styleElement);
79+
sourceAst.walk((node) => fixPostCSSNodeLocation(node, styleElement));
80+
return { status: "success", sourceLang, sourceAst };
81+
}
82+
83+
/**
84+
* Extracts a node location (like that of any ESLint node) from a parsed svelte style node.
85+
*/
86+
export function styleNodeLoc(node: Node): Partial<SourceLocation> {
87+
if (node.source === undefined) {
88+
return {};
89+
}
90+
return {
91+
start:
92+
node.source.start !== undefined
93+
? {
94+
line: node.source.start.line,
95+
column: node.source.start.column - 1,
96+
}
97+
: undefined,
98+
end:
99+
node.source.end !== undefined
100+
? {
101+
line: node.source.end.line,
102+
column: node.source.end.column,
103+
}
104+
: undefined,
105+
};
106+
}
107+
108+
/**
109+
* Extracts a node range (like that of any ESLint node) from a parsed svelte style node.
110+
*/
111+
export function styleNodeRange(
112+
node: Node
113+
): [number | undefined, number | undefined] {
114+
if (node.source === undefined) {
115+
return [undefined, undefined];
116+
}
117+
return [
118+
node.source.start !== undefined ? node.source.start.offset : undefined,
119+
node.source.end !== undefined ? node.source.end.offset + 1 : undefined,
120+
];
121+
}
122+
123+
/**
124+
* Fixes PostCSS AST locations to be relative to the whole file instead of relative to the <style> element.
125+
*/
126+
function fixPostCSSNodeLocation(node: Node, styleElement: SvelteStyleElement) {
127+
if (node.source?.start?.offset !== undefined) {
128+
node.source.start.offset += styleElement.startTag.range[1];
129+
}
130+
if (node.source?.start?.line !== undefined) {
131+
node.source.start.line += styleElement.loc.start.line - 1;
132+
}
133+
if (node.source?.end?.offset !== undefined) {
134+
node.source.end.offset += styleElement.startTag.range[1];
135+
}
136+
if (node.source?.end?.line !== undefined) {
137+
node.source.end.line += styleElement.loc.start.line - 1;
138+
}
139+
if (node.source?.start?.line === styleElement.loc.start.line) {
140+
node.source.start.column += styleElement.startTag.loc.end.column;
141+
}
142+
if (node.source?.end?.line === styleElement.loc.start.line) {
143+
node.source.end.column += styleElement.startTag.loc.end.column;
144+
}
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let a = 10
3+
</script>
4+
5+
<b>{a}</b>
6+
7+
<style></style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"status": "success",
3+
"sourceLang": "css",
4+
"sourceAst": {
5+
"raws": {
6+
"after": ""
7+
},
8+
"type": "root",
9+
"nodes": [],
10+
"source": {
11+
"inputId": 0,
12+
"start": {
13+
"offset": 52,
14+
"line": 7,
15+
"column": 8
16+
}
17+
},
18+
"lastEach": 1,
19+
"indexes": {},
20+
"inputs": [
21+
{
22+
"hasBOM": false,
23+
"css": ""
24+
}
25+
]
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let a = 10
3+
</script>
4+
5+
<b>{a}</b>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"status": "no-style-element"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let a = 10
3+
</script>
4+
5+
<span class="myClass">Hello!</span>
6+
7+
<style> .myClass { color: red; } </style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"status": "success",
3+
"sourceLang": "css",
4+
"sourceAst": {
5+
"raws": {
6+
"semicolon": false,
7+
"after": " "
8+
},
9+
"type": "root",
10+
"nodes": [
11+
{
12+
"raws": {
13+
"before": " ",
14+
"between": " ",
15+
"semicolon": true,
16+
"after": " "
17+
},
18+
"type": "rule",
19+
"nodes": [
20+
{
21+
"raws": {
22+
"before": " ",
23+
"between": ": "
24+
},
25+
"type": "decl",
26+
"source": {
27+
"inputId": 0,
28+
"start": {
29+
"offset": 89,
30+
"line": 7,
31+
"column": 20
32+
},
33+
"end": {
34+
"offset": 99,
35+
"line": 7,
36+
"column": 30
37+
}
38+
},
39+
"prop": "color",
40+
"value": "red"
41+
}
42+
],
43+
"source": {
44+
"inputId": 0,
45+
"start": {
46+
"offset": 78,
47+
"line": 7,
48+
"column": 9
49+
},
50+
"end": {
51+
"offset": 101,
52+
"line": 7,
53+
"column": 32
54+
}
55+
},
56+
"selector": ".myClass",
57+
"lastEach": 1,
58+
"indexes": {}
59+
}
60+
],
61+
"source": {
62+
"inputId": 0,
63+
"start": {
64+
"offset": 77,
65+
"line": 7,
66+
"column": 8
67+
}
68+
},
69+
"lastEach": 1,
70+
"indexes": {},
71+
"inputs": [
72+
{
73+
"hasBOM": false,
74+
"css": " .myClass { color: red; } "
75+
}
76+
]
77+
}
78+
}

0 commit comments

Comments
 (0)
Please sign in to comment.