diff --git a/.changeset/short-ducks-attend.md b/.changeset/short-ducks-attend.md new file mode 100644 index 00000000..8b3bfaa5 --- /dev/null +++ b/.changeset/short-ducks-attend.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": minor +--- + +added PostCSS AST of styles to parser services diff --git a/package.json b/package.json index 5729760e..9c7598dc 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "dependencies": { "eslint-scope": "^7.0.0", "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0" + "espree": "^9.0.0", + "postcss": "^8.4.23", + "postcss-scss": "^4.0.6" }, "devDependencies": { "@changesets/changelog-github": "^0.4.8", diff --git a/src/index.ts b/src/index.ts index f60afaf4..08290598 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,20 @@ -import { parseForESLint } from "./parser"; import * as AST from "./ast"; import { traverseNodes } from "./traverse"; import { KEYS } from "./visitor-keys"; import { ParseError } from "./errors"; +export { + parseForESLint, + StyleContext, + StyleContextNoStyleElement, + StyleContextParseError, + StyleContextSuccess, + StyleContextUnknownLang, +} from "./parser"; export * as meta from "./meta"; export { name } from "./meta"; export { AST, ParseError }; -// parser -export { parseForESLint }; // Keys // eslint-disable-next-line @typescript-eslint/naming-convention -- ignore export const VisitorKeys = KEYS; diff --git a/src/parser/index.ts b/src/parser/index.ts index 72361fa3..49346cf1 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -4,6 +4,7 @@ import type { Comment, SvelteProgram, SvelteScriptElement, + SvelteStyleElement, Token, } from "../ast"; import type { Program } from "estree"; @@ -21,6 +22,24 @@ import { import { ParseError } from "../errors"; import { parseTypeScript } from "./typescript"; import { addReference } from "../scope"; +import { + parseStyleContext, + type StyleContext, + type StyleContextNoStyleElement, + type StyleContextParseError, + type StyleContextSuccess, + type StyleContextUnknownLang, + styleNodeLoc, + styleNodeRange, +} from "./style-context"; + +export { + StyleContext, + StyleContextNoStyleElement, + StyleContextParseError, + StyleContextSuccess, + StyleContextUnknownLang, +}; export interface ESLintProgram extends Program { comments: Comment[]; @@ -50,6 +69,7 @@ export function parseForESLint( services: Record & { isSvelte: true; getSvelteHtmlAst: () => SvAST.Fragment; + getStyleContext: () => StyleContext; }; visitorKeys: { [type: string]: string[] }; scopeManager: ScopeManager; @@ -166,12 +186,22 @@ export function parseForESLint( ); } + const styleElement = ast.body.find( + (b): b is SvelteStyleElement => b.type === "SvelteStyleElement" + ); + const styleContext = parseStyleContext(styleElement, ctx); + resultScript.ast = ast as any; resultScript.services = Object.assign(resultScript.services || {}, { isSvelte: true, getSvelteHtmlAst() { return resultTemplate.svelteAst.html; }, + getStyleContext() { + return styleContext; + }, + styleNodeLoc, + styleNodeRange, }); resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys); diff --git a/src/parser/style-context.ts b/src/parser/style-context.ts new file mode 100644 index 00000000..9df8ff41 --- /dev/null +++ b/src/parser/style-context.ts @@ -0,0 +1,145 @@ +import type { Node, Parser, Root } from "postcss"; +import postcss from "postcss"; +import { parse as SCSSparse } from "postcss-scss"; + +import type { Context } from "../context"; +import type { SourceLocation, SvelteStyleElement } from "../ast"; + +export type StyleContext = + | StyleContextNoStyleElement + | StyleContextParseError + | StyleContextSuccess + | StyleContextUnknownLang; + +export interface StyleContextNoStyleElement { + status: "no-style-element"; +} + +export interface StyleContextParseError { + status: "parse-error"; + sourceLang: string; + error: any; +} + +export interface StyleContextSuccess { + status: "success"; + sourceLang: string; + sourceAst: Root; +} + +export interface StyleContextUnknownLang { + status: "unknown-lang"; + sourceLang: string; +} + +/** + * Extracts style source from a SvelteStyleElement and parses it into a PostCSS AST. + */ +export function parseStyleContext( + styleElement: SvelteStyleElement | undefined, + ctx: Context +): StyleContext { + if (!styleElement || !styleElement.endTag) { + return { status: "no-style-element" }; + } + let sourceLang = "css"; + for (const attribute of styleElement.startTag.attributes) { + if ( + attribute.type === "SvelteAttribute" && + attribute.key.name === "lang" && + attribute.value.length > 0 && + attribute.value[0].type === "SvelteLiteral" + ) { + sourceLang = attribute.value[0].value; + } + } + let parseFn: Parser, sourceAst: Root; + switch (sourceLang) { + case "css": + parseFn = postcss.parse; + break; + case "scss": + parseFn = SCSSparse; + break; + default: + return { status: "unknown-lang", sourceLang }; + } + const styleCode = ctx.code.slice( + styleElement.startTag.range[1], + styleElement.endTag.range[0] + ); + try { + sourceAst = parseFn(styleCode, { + from: ctx.parserOptions.filePath, + }); + } catch (error) { + return { status: "parse-error", sourceLang, error }; + } + fixPostCSSNodeLocation(sourceAst, styleElement); + sourceAst.walk((node) => fixPostCSSNodeLocation(node, styleElement)); + return { status: "success", sourceLang, sourceAst }; +} + +/** + * Extracts a node location (like that of any ESLint node) from a parsed svelte style node. + */ +export function styleNodeLoc(node: Node): Partial { + if (node.source === undefined) { + return {}; + } + return { + start: + node.source.start !== undefined + ? { + line: node.source.start.line, + column: node.source.start.column - 1, + } + : undefined, + end: + node.source.end !== undefined + ? { + line: node.source.end.line, + column: node.source.end.column, + } + : undefined, + }; +} + +/** + * Extracts a node range (like that of any ESLint node) from a parsed svelte style node. + */ +export function styleNodeRange( + node: Node +): [number | undefined, number | undefined] { + if (node.source === undefined) { + return [undefined, undefined]; + } + return [ + node.source.start !== undefined ? node.source.start.offset : undefined, + node.source.end !== undefined ? node.source.end.offset + 1 : undefined, + ]; +} + +/** + * Fixes PostCSS AST locations to be relative to the whole file instead of relative to the diff --git a/tests/fixtures/parser/style-context/empty-style-element-output.json b/tests/fixtures/parser/style-context/empty-style-element-output.json new file mode 100644 index 00000000..2e17f778 --- /dev/null +++ b/tests/fixtures/parser/style-context/empty-style-element-output.json @@ -0,0 +1,27 @@ +{ + "status": "success", + "sourceLang": "css", + "sourceAst": { + "raws": { + "after": "" + }, + "type": "root", + "nodes": [], + "source": { + "inputId": 0, + "start": { + "offset": 52, + "line": 7, + "column": 8 + } + }, + "lastEach": 1, + "indexes": {}, + "inputs": [ + { + "hasBOM": false, + "css": "" + } + ] + } +} diff --git a/tests/fixtures/parser/style-context/no-style-element-input.svelte b/tests/fixtures/parser/style-context/no-style-element-input.svelte new file mode 100644 index 00000000..28c90b28 --- /dev/null +++ b/tests/fixtures/parser/style-context/no-style-element-input.svelte @@ -0,0 +1,5 @@ + + +{a} diff --git a/tests/fixtures/parser/style-context/no-style-element-output.json b/tests/fixtures/parser/style-context/no-style-element-output.json new file mode 100644 index 00000000..10a0894c --- /dev/null +++ b/tests/fixtures/parser/style-context/no-style-element-output.json @@ -0,0 +1,3 @@ +{ + "status": "no-style-element" +} diff --git a/tests/fixtures/parser/style-context/one-line-css-input.svelte b/tests/fixtures/parser/style-context/one-line-css-input.svelte new file mode 100644 index 00000000..0bd7cd09 --- /dev/null +++ b/tests/fixtures/parser/style-context/one-line-css-input.svelte @@ -0,0 +1,7 @@ + + +Hello! + + diff --git a/tests/fixtures/parser/style-context/one-line-css-output.json b/tests/fixtures/parser/style-context/one-line-css-output.json new file mode 100644 index 00000000..669a9c69 --- /dev/null +++ b/tests/fixtures/parser/style-context/one-line-css-output.json @@ -0,0 +1,78 @@ +{ + "status": "success", + "sourceLang": "css", + "sourceAst": { + "raws": { + "semicolon": false, + "after": " " + }, + "type": "root", + "nodes": [ + { + "raws": { + "before": " ", + "between": " ", + "semicolon": true, + "after": " " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": " ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 89, + "line": 7, + "column": 20 + }, + "end": { + "offset": 99, + "line": 7, + "column": 30 + } + }, + "prop": "color", + "value": "red" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 78, + "line": 7, + "column": 9 + }, + "end": { + "offset": 101, + "line": 7, + "column": 32 + } + }, + "selector": ".myClass", + "lastEach": 1, + "indexes": {} + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 77, + "line": 7, + "column": 8 + } + }, + "lastEach": 1, + "indexes": {}, + "inputs": [ + { + "hasBOM": false, + "css": " .myClass { color: red; } " + } + ] + } +} diff --git a/tests/fixtures/parser/style-context/parse-error-input.svelte b/tests/fixtures/parser/style-context/parse-error-input.svelte new file mode 100644 index 00000000..cf61e87d --- /dev/null +++ b/tests/fixtures/parser/style-context/parse-error-input.svelte @@ -0,0 +1,17 @@ +
+
Hello
+ World! +
+ + diff --git a/tests/fixtures/parser/style-context/parse-error-output.json b/tests/fixtures/parser/style-context/parse-error-output.json new file mode 100644 index 00000000..694cc9e0 --- /dev/null +++ b/tests/fixtures/parser/style-context/parse-error-output.json @@ -0,0 +1,20 @@ +{ + "status": "parse-error", + "sourceLang": "css", + "error": { + "name": "CssSyntaxError", + "reason": "Unknown word", + "source": "\n // This syntax is intentionally invalid CSS - this is to be used to test resiliency against invalid input\n .container {\n class .div-class/35\n # Weird comment\n color: red;\n\n .span-class begin\n font-weight: bold;\n end\n }\n", + "line": 4, + "column": 11, + "endLine": 4, + "endColumn": 24, + "input": { + "line": 4, + "column": 11, + "endLine": 4, + "endColumn": 24, + "source": "\n // This syntax is intentionally invalid CSS - this is to be used to test resiliency against invalid input\n .container {\n class .div-class/35\n # Weird comment\n color: red;\n\n .span-class begin\n font-weight: bold;\n end\n }\n" + } + } +} diff --git a/tests/fixtures/parser/style-context/self-closing-style-element-input.svelte b/tests/fixtures/parser/style-context/self-closing-style-element-input.svelte new file mode 100644 index 00000000..3ba7cb97 --- /dev/null +++ b/tests/fixtures/parser/style-context/self-closing-style-element-input.svelte @@ -0,0 +1,7 @@ + + +{a} + + diff --git a/tests/fixtures/parser/style-context/simple-css-output.json b/tests/fixtures/parser/style-context/simple-css-output.json new file mode 100644 index 00000000..103cd5ef --- /dev/null +++ b/tests/fixtures/parser/style-context/simple-css-output.json @@ -0,0 +1,127 @@ +{ + "status": "success", + "sourceLang": "css", + "sourceAst": { + "raws": { + "semicolon": false, + "after": "\n" + }, + "type": "root", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": " ", + "semicolon": true, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 107, + "line": 11, + "column": 5 + }, + "end": { + "offset": 117, + "line": 11, + "column": 15 + } + }, + "prop": "color", + "value": "red" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 92, + "line": 10, + "column": 3 + }, + "end": { + "offset": 121, + "line": 12, + "column": 3 + } + }, + "selector": ".myClass", + "lastEach": 1, + "indexes": {} + }, + { + "raws": { + "before": "\n\n ", + "between": " ", + "semicolon": true, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 134, + "line": 15, + "column": 5 + }, + "end": { + "offset": 153, + "line": 15, + "column": 24 + } + }, + "prop": "font-size", + "value": "xx-large" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 126, + "line": 14, + "column": 3 + }, + "end": { + "offset": 157, + "line": 16, + "column": 3 + } + }, + "selector": "b", + "lastEach": 1, + "indexes": {} + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 89, + "line": 9, + "column": 8 + } + }, + "lastEach": 1, + "indexes": {}, + "inputs": [ + { + "hasBOM": false, + "css": "\n .myClass {\n color: red;\n }\n\n b {\n font-size: xx-large;\n }\n" + } + ] + } +} diff --git a/tests/fixtures/parser/style-context/simple-scss-input.svelte b/tests/fixtures/parser/style-context/simple-scss-input.svelte new file mode 100644 index 00000000..45bbe860 --- /dev/null +++ b/tests/fixtures/parser/style-context/simple-scss-input.svelte @@ -0,0 +1,18 @@ +
+
Hello
+ + World! +
+ + diff --git a/tests/fixtures/parser/style-context/simple-scss-output.json b/tests/fixtures/parser/style-context/simple-scss-output.json new file mode 100644 index 00000000..a3c34918 --- /dev/null +++ b/tests/fixtures/parser/style-context/simple-scss-output.json @@ -0,0 +1,178 @@ +{ + "status": "success", + "sourceLang": "scss", + "sourceAst": { + "raws": { + "semicolon": false, + "after": "\n" + }, + "type": "root", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": " ", + "semicolon": false, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": " ", + "semicolon": true, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "inline": true, + "left": " ", + "right": "", + "text": "This is an inline comment" + }, + "type": "comment", + "source": { + "inputId": 0, + "start": { + "offset": 169, + "line": 10, + "column": 7 + }, + "end": { + "offset": 196, + "line": 10, + "column": 34 + } + }, + "text": "This is an inline comment" + }, + { + "raws": { + "before": "\n ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 204, + "line": 11, + "column": 7 + }, + "end": { + "offset": 214, + "line": 11, + "column": 17 + } + }, + "prop": "color", + "value": "red" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 150, + "line": 9, + "column": 5 + }, + "end": { + "offset": 220, + "line": 12, + "column": 5 + } + }, + "selector": ".div-class", + "lastEach": 1, + "indexes": {} + }, + { + "raws": { + "before": "\n\n ", + "between": " ", + "semicolon": true, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 247, + "line": 15, + "column": 7 + }, + "end": { + "offset": 264, + "line": 15, + "column": 24 + } + }, + "prop": "font-weight", + "value": "bold" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 227, + "line": 14, + "column": 5 + }, + "end": { + "offset": 270, + "line": 16, + "column": 5 + } + }, + "selector": ".span-class", + "lastEach": 1, + "indexes": {} + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 133, + "line": 8, + "column": 3 + }, + "end": { + "offset": 274, + "line": 17, + "column": 3 + } + }, + "selector": ".container", + "lastEach": 1, + "indexes": {} + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 130, + "line": 7, + "column": 20 + } + }, + "lastEach": 1, + "indexes": {}, + "inputs": [ + { + "hasBOM": false, + "css": "\n .container {\n .div-class {\n // This is an inline comment\n color: red;\n }\n\n .span-class {\n font-weight: bold;\n }\n }\n" + } + ] + } +} diff --git a/tests/fixtures/parser/style-context/unknown-lang-input.svelte b/tests/fixtures/parser/style-context/unknown-lang-input.svelte new file mode 100644 index 00000000..0c3b211e --- /dev/null +++ b/tests/fixtures/parser/style-context/unknown-lang-input.svelte @@ -0,0 +1,17 @@ +
+
Hello
+ World! +
+ + diff --git a/tests/fixtures/parser/style-context/unknown-lang-output.json b/tests/fixtures/parser/style-context/unknown-lang-output.json new file mode 100644 index 00000000..89682316 --- /dev/null +++ b/tests/fixtures/parser/style-context/unknown-lang-output.json @@ -0,0 +1,4 @@ +{ + "status": "unknown-lang", + "sourceLang": "invalid-lang" +} diff --git a/tests/fixtures/parser/style-context/unrelated-style-attr-input.svelte b/tests/fixtures/parser/style-context/unrelated-style-attr-input.svelte new file mode 100644 index 00000000..cb0a6f96 --- /dev/null +++ b/tests/fixtures/parser/style-context/unrelated-style-attr-input.svelte @@ -0,0 +1,17 @@ + + +Hello! + +{a} + + diff --git a/tests/fixtures/parser/style-context/unrelated-style-attr-output.json b/tests/fixtures/parser/style-context/unrelated-style-attr-output.json new file mode 100644 index 00000000..1d6c539a --- /dev/null +++ b/tests/fixtures/parser/style-context/unrelated-style-attr-output.json @@ -0,0 +1,127 @@ +{ + "status": "success", + "sourceLang": "css", + "sourceAst": { + "raws": { + "semicolon": false, + "after": "\n" + }, + "type": "root", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": " ", + "semicolon": true, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 124, + "line": 11, + "column": 5 + }, + "end": { + "offset": 134, + "line": 11, + "column": 15 + } + }, + "prop": "color", + "value": "red" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 109, + "line": 10, + "column": 3 + }, + "end": { + "offset": 138, + "line": 12, + "column": 3 + } + }, + "selector": ".myClass", + "lastEach": 1, + "indexes": {} + }, + { + "raws": { + "before": "\n\n ", + "between": " ", + "semicolon": true, + "after": "\n " + }, + "type": "rule", + "nodes": [ + { + "raws": { + "before": "\n ", + "between": ": " + }, + "type": "decl", + "source": { + "inputId": 0, + "start": { + "offset": 151, + "line": 15, + "column": 5 + }, + "end": { + "offset": 170, + "line": 15, + "column": 24 + } + }, + "prop": "font-size", + "value": "xx-large" + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 143, + "line": 14, + "column": 3 + }, + "end": { + "offset": 174, + "line": 16, + "column": 3 + } + }, + "selector": "b", + "lastEach": 1, + "indexes": {} + } + ], + "source": { + "inputId": 0, + "start": { + "offset": 106, + "line": 9, + "column": 25 + } + }, + "lastEach": 1, + "indexes": {}, + "inputs": [ + { + "hasBOM": false, + "css": "\n .myClass {\n color: red;\n }\n\n b {\n font-size: xx-large;\n }\n" + } + ] + } +} diff --git a/tests/fixtures/parser/style-location-converter/simple-css-input.svelte b/tests/fixtures/parser/style-location-converter/simple-css-input.svelte new file mode 100644 index 00000000..b23a3310 --- /dev/null +++ b/tests/fixtures/parser/style-location-converter/simple-css-input.svelte @@ -0,0 +1,17 @@ + + +Hello! + +{a} + + diff --git a/tests/fixtures/parser/style-location-converter/simple-css-output.json b/tests/fixtures/parser/style-location-converter/simple-css-output.json new file mode 100644 index 00000000..af8eed52 --- /dev/null +++ b/tests/fixtures/parser/style-location-converter/simple-css-output.json @@ -0,0 +1,78 @@ +[ + [ + { + "start": { + "line": 9, + "column": 7 + } + }, + [ + 89, + null + ] + ], + [ + { + "start": { + "line": 10, + "column": 2 + }, + "end": { + "line": 12, + "column": 3 + } + }, + [ + 92, + 122 + ] + ], + [ + { + "start": { + "line": 11, + "column": 4 + }, + "end": { + "line": 11, + "column": 15 + } + }, + [ + 107, + 118 + ] + ], + [ + { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 16, + "column": 3 + } + }, + [ + 126, + 158 + ] + ], + [ + { + "start": { + "line": 15, + "column": 4 + }, + "end": { + "line": 15, + "column": 24 + } + }, + [ + 134, + 154 + ] + ] +] diff --git a/tests/fixtures/parser/style-location-converter/simple-scss-input.svelte b/tests/fixtures/parser/style-location-converter/simple-scss-input.svelte new file mode 100644 index 00000000..45bbe860 --- /dev/null +++ b/tests/fixtures/parser/style-location-converter/simple-scss-input.svelte @@ -0,0 +1,18 @@ +
+
Hello
+ + World! +
+ + diff --git a/tests/fixtures/parser/style-location-converter/simple-scss-output.json b/tests/fixtures/parser/style-location-converter/simple-scss-output.json new file mode 100644 index 00000000..2e622e5c --- /dev/null +++ b/tests/fixtures/parser/style-location-converter/simple-scss-output.json @@ -0,0 +1,110 @@ +[ + [ + { + "start": { + "line": 7, + "column": 19 + } + }, + [ + 130, + null + ] + ], + [ + { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 17, + "column": 3 + } + }, + [ + 133, + 275 + ] + ], + [ + { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 12, + "column": 5 + } + }, + [ + 150, + 221 + ] + ], + [ + { + "start": { + "line": 10, + "column": 6 + }, + "end": { + "line": 10, + "column": 34 + } + }, + [ + 169, + 197 + ] + ], + [ + { + "start": { + "line": 11, + "column": 6 + }, + "end": { + "line": 11, + "column": 17 + } + }, + [ + 204, + 215 + ] + ], + [ + { + "start": { + "line": 14, + "column": 4 + }, + "end": { + "line": 16, + "column": 5 + } + }, + [ + 227, + 271 + ] + ], + [ + { + "start": { + "line": 15, + "column": 6 + }, + "end": { + "line": 15, + "column": 24 + } + }, + [ + 247, + 265 + ] + ] +] diff --git a/tests/src/parser/style-context.ts b/tests/src/parser/style-context.ts new file mode 100644 index 00000000..157e3539 --- /dev/null +++ b/tests/src/parser/style-context.ts @@ -0,0 +1,51 @@ +import assert from "assert"; +import fs from "fs"; +import path from "path"; + +import { parseForESLint } from "../../../src"; +import type { StyleContext } from "../../../src/parser"; +import { generateParserOptions, listupFixtures } from "./test-utils"; + +const STYLE_CONTEXT_FIXTURE_ROOT = path.resolve( + __dirname, + "../../fixtures/parser/style-context" +); + +function parse(code: string, filePath: string, config: any) { + return parseForESLint(code, generateParserOptions({ filePath }, config)); +} + +describe("Check for AST.", () => { + for (const { + input, + inputFileName, + outputFileName, + config, + meetRequirements, + } of listupFixtures(STYLE_CONTEXT_FIXTURE_ROOT)) { + describe(inputFileName, () => { + let result: any; + + it("most to generate the expected style context.", () => { + result = parse(input, inputFileName, config); + if (!meetRequirements("test")) { + return; + } + const styleContext = result.services.getStyleContext(); + const output = fs.readFileSync(outputFileName, "utf8"); + assert.strictEqual(`${styleContextToJson(styleContext)}\n`, output); + }); + }); + } +}); + +function styleContextToJson(styleContext: StyleContext): string { + return JSON.stringify(styleContext, nodeReplacer, 2); +} + +function nodeReplacer(key: string, value: any): any { + if (key === "file" || key === "url") { + return undefined; + } + return value; +} diff --git a/tests/src/parser/style-location-coverter.ts b/tests/src/parser/style-location-coverter.ts new file mode 100644 index 00000000..361aa507 --- /dev/null +++ b/tests/src/parser/style-location-coverter.ts @@ -0,0 +1,60 @@ +import assert from "assert"; +import fs from "fs"; +import path from "path"; +import type { Node } from "postcss"; + +import { parseForESLint } from "../../../src"; +import type { SourceLocation } from "../../../src/ast"; +import { generateParserOptions, listupFixtures } from "./test-utils"; + +const STYLE_CONTEXT_FIXTURE_ROOT = path.resolve( + __dirname, + "../../fixtures/parser/style-location-converter" +); + +function parse(code: string, filePath: string, config: any) { + return parseForESLint(code, generateParserOptions({ filePath }, config)); +} + +describe("Check for AST.", () => { + for (const { + input, + inputFileName, + outputFileName, + config, + meetRequirements, + } of listupFixtures(STYLE_CONTEXT_FIXTURE_ROOT)) { + describe(inputFileName, () => { + let services: any; + + it("most to generate the expected style context.", () => { + services = parse(input, inputFileName, config).services; + if (!meetRequirements("test")) { + return; + } + const styleContext = services.getStyleContext(); + assert.strictEqual(styleContext.status, "success"); + const locations: [ + Partial, + [number | undefined, number | undefined] + ][] = [ + [ + services.styleNodeLoc(styleContext.sourceAst), + services.styleNodeRange(styleContext.sourceAst), + ], + ]; + styleContext.sourceAst.walk((node: Node) => { + locations.push([ + services.styleNodeLoc(node), + services.styleNodeRange(node), + ]); + }); + const output = fs.readFileSync(outputFileName, "utf8"); + assert.strictEqual( + `${JSON.stringify(locations, undefined, 2)}\n`, + output + ); + }); + }); + } +});