Skip to content

Commit

Permalink
Added support for style selector parsing (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
marekdedic authored Dec 31, 2024
1 parent fd076a4 commit 002e3b0
Show file tree
Hide file tree
Showing 25 changed files with 2,793 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-melons-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": minor
---

feat: added support for style selector parsing
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"eslint-visitor-keys": "^4.0.0",
"espree": "^10.0.0",
"postcss": "^8.4.49",
"postcss-scss": "^4.0.9"
"postcss-scss": "^4.0.9",
"postcss-selector-parser": "^7.0.0"
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
Expand Down
25 changes: 25 additions & 0 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { KEYS } from "../visitor-keys.js";
import { Context } from "../context/index.js";
import type {
Comment,
SourceLocation,
SvelteProgram,
SvelteScriptElement,
SvelteStyleElement,
Expand All @@ -10,6 +11,11 @@ import type {
import type { Program } from "estree";
import type { ScopeManager } from "eslint-scope";
import { Variable } from "eslint-scope";
import type { Rule, Node } from "postcss";
import type {
Node as SelectorNode,
Root as SelectorRoot,
} from "postcss-selector-parser";
import { parseScript, parseScriptInSvelte } from "./script.js";
import type * as SvAST from "./svelte-ast-types.js";
import type * as Compiler from "./svelte-ast-types-for-v5.js";
Expand All @@ -29,13 +35,15 @@ import {
import { addReference } from "../scope/index.js";
import {
parseStyleContext,
parseSelector,
type StyleContext,
type StyleContextNoStyleElement,
type StyleContextParseError,
type StyleContextSuccess,
type StyleContextUnknownLang,
styleNodeLoc,
styleNodeRange,
styleSelectorNodeLoc,
} from "./style-context.js";
import { getGlobalsForSvelte, getGlobalsForSvelteScript } from "./globals.js";
import type { NormalizedParserOptions } from "./parser-options.js";
Expand Down Expand Up @@ -84,6 +92,12 @@ type ParseResult = {
isSvelteScript: false;
getSvelteHtmlAst: () => SvAST.Fragment | Compiler.Fragment;
getStyleContext: () => StyleContext;
getStyleSelectorAST: (rule: Rule) => SelectorRoot;
styleNodeLoc: (node: Node) => Partial<SourceLocation>;
styleNodeRange: (
node: Node,
) => [number | undefined, number | undefined];
styleSelectorNodeLoc: (node: SelectorNode) => Partial<SourceLocation>;
svelteParseContext: SvelteParseContext;
}
| {
Expand Down Expand Up @@ -221,6 +235,7 @@ function parseAsSvelte(
(b): b is SvelteStyleElement => b.type === "SvelteStyleElement",
);
let styleContext: StyleContext | null = null;
const selectorASTs: Map<Rule, SelectorRoot> = new Map();

resultScript.ast = ast as any;
resultScript.services = Object.assign(resultScript.services || {}, {
Expand All @@ -235,8 +250,18 @@ function parseAsSvelte(
}
return styleContext;
},
getStyleSelectorAST(rule: Rule) {
const cached = selectorASTs.get(rule);
if (cached !== undefined) {
return cached;
}
const ast = parseSelector(rule);
selectorASTs.set(rule, ast);
return ast;
},
styleNodeLoc,
styleNodeRange,
styleSelectorNodeLoc,
svelteParseContext,
});
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);
Expand Down
70 changes: 68 additions & 2 deletions src/parser/style-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Node, Parser, Root } from "postcss";
import type { Node, Parser, Root, Rule } from "postcss";
import postcss from "postcss";
import { parse as SCSSparse } from "postcss-scss";
import {
default as selectorParser,
type Node as SelectorNode,
type Root as SelectorRoot,
} from "postcss-selector-parser";

import type { Context } from "../context/index.js";
import type { SourceLocation, SvelteStyleElement } from "../ast/index.js";
Expand Down Expand Up @@ -77,10 +82,25 @@ export function parseStyleContext(
return { status: "parse-error", sourceLang, error };
}
fixPostCSSNodeLocation(sourceAst, styleElement);
sourceAst.walk((node) => fixPostCSSNodeLocation(node, styleElement));
sourceAst.walk((node) => {
fixPostCSSNodeLocation(node, styleElement);
});
return { status: "success", sourceLang, sourceAst };
}

/**
* Parses a PostCSS Rule node's selector and returns its AST.
*/
export function parseSelector(rule: Rule): SelectorRoot {
const processor = selectorParser();
const root = processor.astSync(rule.selector);
fixSelectorNodeLocation(root, rule);
root.walk((node) => {
fixSelectorNodeLocation(node, rule);
});
return root;
}

/**
* Extracts a node location (like that of any ESLint node) from a parsed svelte style node.
*/
Expand Down Expand Up @@ -121,6 +141,24 @@ export function styleNodeRange(
];
}

/**
* Extracts a node location (like that of any ESLint node) from a parsed svelte selector node.
*/
export function styleSelectorNodeLoc(
node: SelectorNode,
): Partial<SourceLocation> {
return {
start:
node.source?.start !== undefined
? {
line: node.source.start.line,
column: node.source.start.column - 1,
}
: undefined,
end: node.source?.end,
};
}

/**
* Fixes PostCSS AST locations to be relative to the whole file instead of relative to the <style> element.
*/
Expand All @@ -144,3 +182,31 @@ function fixPostCSSNodeLocation(node: Node, styleElement: SvelteStyleElement) {
node.source.end.column += styleElement.startTag.loc.end.column;
}
}

/**
* Fixes selector AST locations to be relative to the whole file instead of relative to their parent rule.
*/
function fixSelectorNodeLocation(node: SelectorNode, rule: Rule) {
if (node.source === undefined) {
return;
}
const ruleLoc = styleNodeLoc(rule);

if (node.source.start !== undefined && ruleLoc.start !== undefined) {
if (node.source.start.line === 1) {
node.source.start.column += ruleLoc.start.column;
}
node.source.start.line += ruleLoc.start.line - 1;
} else {
node.source.start = undefined;
}

if (node.source.end !== undefined && ruleLoc.start !== undefined) {
if (node.source.end.line === 1) {
node.source.end.column += ruleLoc.start.column;
}
node.source.end.line += ruleLoc.start.line - 1;
} else {
node.source.end = undefined;
}
}
25 changes: 25 additions & 0 deletions tests/fixtures/parser/selector-parsing/simple-css-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script>
let a = 10
</script>

<span class="myClass">Hello!</span>

<b>{a}</b>

<style>
.myClass {
color: red;
}
b {
font-size: xx-large;
}
a:active,
a::before,
b + a,
b + .myClass,
a[data-key="value"] {
color: blue;
}
</style>
Loading

0 comments on commit 002e3b0

Please sign in to comment.