Skip to content

Added support for style selector parsing #619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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",
25 changes: 25 additions & 0 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { KEYS } from "../visitor-keys.js";
import { Context } from "../context/index.js";
import type {
Comment,
SourceLocation,
SvelteProgram,
SvelteScriptElement,
SvelteStyleElement,
@@ -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";
@@ -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";
@@ -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;
}
| {
@@ -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 || {}, {
@@ -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);
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";
@@ -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.
*/
@@ -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,
};
}

Comment on lines +147 to +160
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you do this in fixSelectorNodeLocation? I thought the loc information could always be handled in ESLint style because this program runs on ESLint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm following the same approach we agreed on in #340 (comment) - the TL;DR is that PostCSS AST locations are different than ESLint AST locations - one of them starts counting columns from 0 and other from 1, one of them has the end point at the last character while the other has it point at the first character after the end... So we agreed we would recalculate the positions to be relative to the whole file instead of being relative to the selector, but we would keep the PostCSS semantics in the PostCSS AST. Only when converting from PostCSS AST to an ESLint SourceLocation would we convert this to the ESLint way of doing things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course this is debatable (there is no universally "right" solution IMO), but I'd strongly suggest doing the selector parsing in the same way as the style node parsing. So if we were to change this one, we'd need to change the other one too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for explanation! OK. So let's merge this PR now!

/**
* Fixes PostCSS AST locations to be relative to the whole file instead of relative to the <style> element.
*/
@@ -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>
494 changes: 494 additions & 0 deletions tests/fixtures/parser/selector-parsing/simple-css-output.json

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions tests/fixtures/parser/selector-parsing/simple-postcss-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="container">
<div class="div-class">Hello</div>

<span class="span-class">World!</span>
</div>

<style lang="postcss">
body {
colour: white;
background-colour: grey;
}
a:active,
a::before,
b + a,
b + .myClass,
a[data-key="value"] {
color: blue;
}
</style>
433 changes: 433 additions & 0 deletions tests/fixtures/parser/selector-parsing/simple-postcss-output.json

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions tests/fixtures/parser/selector-parsing/simple-scss-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div class="container">
<div class="div-class">Hello</div>

<span class="span-class">World!</span>
</div>

<style lang="scss">
.container {
.div-class {
// This is an inline comment
color: red;
}
.span-class {
font-weight: bold;
}
a:active,
a::before,
b + a,
b + .myClass,
a[data-key="value"] {
color: blue;
}
}
</style>
554 changes: 554 additions & 0 deletions tests/fixtures/parser/selector-parsing/simple-scss-output.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
[
"root",
{
"start": {
"line": 9,
@@ -16,6 +17,7 @@
]
],
[
"rule",
{
"start": {
"line": 10,
@@ -32,6 +34,7 @@
]
],
[
"decl",
{
"start": {
"line": 11,
@@ -48,6 +51,7 @@
]
],
[
"rule",
{
"start": {
"line": 14,
@@ -64,6 +68,7 @@
]
],
[
"decl",
{
"start": {
"line": 15,
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
[
"root",
{
"start": {
"line": 7,
@@ -16,6 +17,7 @@
]
],
[
"rule",
{
"start": {
"line": 8,
@@ -32,6 +34,7 @@
]
],
[
"decl",
{
"start": {
"line": 9,
@@ -48,6 +51,7 @@
]
],
[
"decl",
{
"start": {
"line": 10,
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
[
"root",
{
"start": {
"line": 7,
@@ -16,6 +17,7 @@
]
],
[
"rule",
{
"start": {
"line": 8,
@@ -32,6 +34,7 @@
]
],
[
"rule",
{
"start": {
"line": 9,
@@ -48,6 +51,7 @@
]
],
[
"comment",
{
"start": {
"line": 10,
@@ -64,6 +68,7 @@
]
],
[
"decl",
{
"start": {
"line": 11,
@@ -80,6 +85,7 @@
]
],
[
"rule",
{
"start": {
"line": 14,
@@ -96,6 +102,7 @@
]
],
[
"decl",
{
"start": {
"line": 15,
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
[
[
[
"selector",
{
"start": {
"line": 10,
"column": 2
},
"end": {
"line": 10,
"column": 10
}
}
],
[
"class",
{
"start": {
"line": 10,
"column": 2
},
"end": {
"line": 10,
"column": 10
}
}
]
],
[
[
"selector",
{
"start": {
"line": 14,
"column": 2
},
"end": {
"line": 14,
"column": 3
}
}
],
[
"tag",
{
"start": {
"line": 14,
"column": 2
},
"end": {
"line": 14,
"column": 3
}
}
]
],
[
[
"selector",
{
"start": {
"line": 18,
"column": 2
},
"end": {
"line": 18,
"column": 10
}
}
],
[
"tag",
{
"start": {
"line": 18,
"column": 2
},
"end": {
"line": 18,
"column": 3
}
}
],
[
"pseudo",
{
"start": {
"line": 18,
"column": 3
},
"end": {
"line": 18,
"column": 10
}
}
],
[
"selector",
{
"start": {
"line": 19,
"column": -1
},
"end": {
"line": 19,
"column": 11
}
}
],
[
"tag",
{
"start": {
"line": 19,
"column": 2
},
"end": {
"line": 19,
"column": 3
}
}
],
[
"pseudo",
{
"start": {
"line": 19,
"column": 3
},
"end": {
"line": 19,
"column": 11
}
}
],
[
"selector",
{
"start": {
"line": 20,
"column": -1
},
"end": {
"line": 20,
"column": 7
}
}
],
[
"tag",
{
"start": {
"line": 20,
"column": 2
},
"end": {
"line": 20,
"column": 3
}
}
],
[
"combinator",
{
"start": {
"line": 20,
"column": 4
},
"end": {
"line": 20,
"column": 5
}
}
],
[
"tag",
{
"start": {
"line": 20,
"column": 6
},
"end": {
"line": 20,
"column": 7
}
}
],
[
"selector",
{
"start": {
"line": 21,
"column": -1
},
"end": {
"line": 21,
"column": 14
}
}
],
[
"tag",
{
"start": {
"line": 21,
"column": 2
},
"end": {
"line": 21,
"column": 3
}
}
],
[
"combinator",
{
"start": {
"line": 21,
"column": 4
},
"end": {
"line": 21,
"column": 5
}
}
],
[
"class",
{
"start": {
"line": 21,
"column": 6
},
"end": {
"line": 21,
"column": 14
}
}
],
[
"selector",
{
"start": {
"line": 22,
"column": -1
},
"end": {
"line": 22,
"column": 21
}
}
],
[
"tag",
{
"start": {
"line": 22,
"column": 2
},
"end": {
"line": 22,
"column": 3
}
}
],
[
"attribute",
{
"start": {
"line": 22,
"column": 3
},
"end": {
"line": 22,
"column": 21
}
}
]
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="container">
<div class="div-class">Hello</div>

<span class="span-class">World!</span>
</div>

<style lang="postcss">
body {
colour: white;
background-colour: grey;
}
a:active,
a::before,
b + a,
b + .myClass,
a[data-key="value"] {
color: blue;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
[
[
[
"selector",
{
"start": {
"line": 8,
"column": 2
},
"end": {
"line": 8,
"column": 6
}
}
],
[
"tag",
{
"start": {
"line": 8,
"column": 2
},
"end": {
"line": 8,
"column": 6
}
}
]
],
[
[
"selector",
{
"start": {
"line": 13,
"column": 2
},
"end": {
"line": 13,
"column": 10
}
}
],
[
"tag",
{
"start": {
"line": 13,
"column": 2
},
"end": {
"line": 13,
"column": 3
}
}
],
[
"pseudo",
{
"start": {
"line": 13,
"column": 3
},
"end": {
"line": 13,
"column": 10
}
}
],
[
"selector",
{
"start": {
"line": 14,
"column": -1
},
"end": {
"line": 14,
"column": 11
}
}
],
[
"tag",
{
"start": {
"line": 14,
"column": 2
},
"end": {
"line": 14,
"column": 3
}
}
],
[
"pseudo",
{
"start": {
"line": 14,
"column": 3
},
"end": {
"line": 14,
"column": 11
}
}
],
[
"selector",
{
"start": {
"line": 15,
"column": -1
},
"end": {
"line": 15,
"column": 7
}
}
],
[
"tag",
{
"start": {
"line": 15,
"column": 2
},
"end": {
"line": 15,
"column": 3
}
}
],
[
"combinator",
{
"start": {
"line": 15,
"column": 4
},
"end": {
"line": 15,
"column": 5
}
}
],
[
"tag",
{
"start": {
"line": 15,
"column": 6
},
"end": {
"line": 15,
"column": 7
}
}
],
[
"selector",
{
"start": {
"line": 16,
"column": -1
},
"end": {
"line": 16,
"column": 14
}
}
],
[
"tag",
{
"start": {
"line": 16,
"column": 2
},
"end": {
"line": 16,
"column": 3
}
}
],
[
"combinator",
{
"start": {
"line": 16,
"column": 4
},
"end": {
"line": 16,
"column": 5
}
}
],
[
"class",
{
"start": {
"line": 16,
"column": 6
},
"end": {
"line": 16,
"column": 14
}
}
],
[
"selector",
{
"start": {
"line": 17,
"column": -1
},
"end": {
"line": 17,
"column": 21
}
}
],
[
"tag",
{
"start": {
"line": 17,
"column": 2
},
"end": {
"line": 17,
"column": 3
}
}
],
[
"attribute",
{
"start": {
"line": 17,
"column": 3
},
"end": {
"line": 17,
"column": 21
}
}
]
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div class="container">
<div class="div-class">Hello</div>

<span class="span-class">World!</span>
</div>

<style lang="scss">
.container {
.div-class {
// This is an inline comment
color: red;
}
.span-class {
font-weight: bold;
}
a:active,
a::before,
b + a,
b + .myClass,
a[data-key="value"] {
color: blue;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
[
[
[
"selector",
{
"start": {
"line": 8,
"column": 2
},
"end": {
"line": 8,
"column": 12
}
}
],
[
"class",
{
"start": {
"line": 8,
"column": 2
},
"end": {
"line": 8,
"column": 12
}
}
]
],
[
[
"selector",
{
"start": {
"line": 9,
"column": 4
},
"end": {
"line": 9,
"column": 14
}
}
],
[
"class",
{
"start": {
"line": 9,
"column": 4
},
"end": {
"line": 9,
"column": 14
}
}
]
],
[
[
"selector",
{
"start": {
"line": 14,
"column": 4
},
"end": {
"line": 14,
"column": 15
}
}
],
[
"class",
{
"start": {
"line": 14,
"column": 4
},
"end": {
"line": 14,
"column": 15
}
}
]
],
[
[
"selector",
{
"start": {
"line": 18,
"column": 4
},
"end": {
"line": 18,
"column": 12
}
}
],
[
"tag",
{
"start": {
"line": 18,
"column": 4
},
"end": {
"line": 18,
"column": 5
}
}
],
[
"pseudo",
{
"start": {
"line": 18,
"column": 5
},
"end": {
"line": 18,
"column": 12
}
}
],
[
"selector",
{
"start": {
"line": 19,
"column": -1
},
"end": {
"line": 19,
"column": 13
}
}
],
[
"tag",
{
"start": {
"line": 19,
"column": 4
},
"end": {
"line": 19,
"column": 5
}
}
],
[
"pseudo",
{
"start": {
"line": 19,
"column": 5
},
"end": {
"line": 19,
"column": 13
}
}
],
[
"selector",
{
"start": {
"line": 20,
"column": -1
},
"end": {
"line": 20,
"column": 9
}
}
],
[
"tag",
{
"start": {
"line": 20,
"column": 4
},
"end": {
"line": 20,
"column": 5
}
}
],
[
"combinator",
{
"start": {
"line": 20,
"column": 6
},
"end": {
"line": 20,
"column": 7
}
}
],
[
"tag",
{
"start": {
"line": 20,
"column": 8
},
"end": {
"line": 20,
"column": 9
}
}
],
[
"selector",
{
"start": {
"line": 21,
"column": -1
},
"end": {
"line": 21,
"column": 16
}
}
],
[
"tag",
{
"start": {
"line": 21,
"column": 4
},
"end": {
"line": 21,
"column": 5
}
}
],
[
"combinator",
{
"start": {
"line": 21,
"column": 6
},
"end": {
"line": 21,
"column": 7
}
}
],
[
"class",
{
"start": {
"line": 21,
"column": 8
},
"end": {
"line": 21,
"column": 16
}
}
],
[
"selector",
{
"start": {
"line": 22,
"column": -1
},
"end": {
"line": 22,
"column": 23
}
}
],
[
"tag",
{
"start": {
"line": 22,
"column": 4
},
"end": {
"line": 22,
"column": 5
}
}
],
[
"attribute",
{
"start": {
"line": 22,
"column": 5
},
"end": {
"line": 22,
"column": 23
}
}
]
]
]
56 changes: 56 additions & 0 deletions tests/src/parser/selector-parsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import assert from "assert";
import fs from "fs";
import path from "path";
import type { Node } from "postcss";
import type { Root as SelectorRoot } from "postcss-selector-parser";

import { parseForESLint } from "../../../src/index.js";
import {
generateParserOptions,
listupFixtures,
selectorAstToJson,
} from "./test-utils.js";

const dirname = path.dirname(new URL(import.meta.url).pathname);
const SELECTOR_PARSING_FIXTURE_ROOT = path.resolve(
dirname,
"../../fixtures/parser/selector-parsing",
);

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(SELECTOR_PARSING_FIXTURE_ROOT)) {
if (!meetRequirements("parse")) {
continue;
}
describe(inputFileName, () => {
let services: any;

it("most to generate the expected selector AST.", () => {
services = parse(input, inputFileName, config).services;
if (!meetRequirements("test")) {
return;
}
const styleContext = services.getStyleContext();
assert.strictEqual(styleContext.status, "success");
const selectorASTs: SelectorRoot[] = [];
styleContext.sourceAst.walk((node: Node) => {
if (node.type === "rule") {
selectorASTs.push(services.getStyleSelectorAST(node));
}
});
const output = fs.readFileSync(outputFileName, "utf8");
assert.strictEqual(`${selectorAstToJson(selectorASTs)}\n`, output);
});
});
}
});
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import { generateParserOptions, listupFixtures } from "./test-utils.js";
import type { SourceLocation } from "../../../src/ast/common.js";

const dirname = path.dirname(new URL(import.meta.url).pathname);
const STYLE_CONTEXT_FIXTURE_ROOT = path.resolve(
const STYLE_LOCATION_CONVERTER_FIXTURE_ROOT = path.resolve(
dirname,
"../../fixtures/parser/style-location-converter",
);
@@ -24,7 +24,7 @@ describe("Check for AST.", () => {
outputFileName,
config,
meetRequirements,
} of listupFixtures(STYLE_CONTEXT_FIXTURE_ROOT)) {
} of listupFixtures(STYLE_LOCATION_CONVERTER_FIXTURE_ROOT)) {
describe(inputFileName, () => {
let services: any;

@@ -36,16 +36,19 @@ describe("Check for AST.", () => {
const styleContext = services.getStyleContext();
assert.strictEqual(styleContext.status, "success");
const locations: [
string,
Partial<SourceLocation>,
[number | undefined, number | undefined],
][] = [
[
"root",
services.styleNodeLoc(styleContext.sourceAst),
services.styleNodeRange(styleContext.sourceAst),
],
];
styleContext.sourceAst.walk((node: Node) => {
locations.push([
node.type,
services.styleNodeLoc(node),
services.styleNodeRange(node),
]);
25 changes: 25 additions & 0 deletions tests/src/parser/style-selector-location-converter-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { AnyNode, Root } from "postcss";
import type { Node as SelectorNode } from "postcss-selector-parser";

import type { SourceLocation } from "../../../src/ast/common.js";

export function extractSelectorLocations(
services: Record<string, any>,
styleAST: Root,
): [string, Partial<SourceLocation>][][] {
const locations: [string, Partial<SourceLocation>][][] = [];
styleAST.walk((node: AnyNode) => {
if (node.type === "rule") {
const selectorAst = services.getStyleSelectorAST(node);
const selectorLocations: [string, Partial<SourceLocation>][] = [];
selectorAst.walk((selectorNode: SelectorNode) => {
selectorLocations.push([
selectorNode.type,
services.styleSelectorNodeLoc(selectorNode, node),
]);
});
locations.push(selectorLocations);
}
});
return locations;
}
49 changes: 49 additions & 0 deletions tests/src/parser/style-selector-location-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from "assert";
import fs from "fs";
import path from "path";

import { parseForESLint } from "../../../src/index.js";
import { extractSelectorLocations } from "./style-selector-location-converter-utils.js";
import { generateParserOptions, listupFixtures } from "./test-utils.js";

const dirname = path.dirname(new URL(import.meta.url).pathname);
const SELECTOR_CONVERTER_FIXTURE_ROOT = path.resolve(
dirname,
"../../fixtures/parser/style-selector-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(SELECTOR_CONVERTER_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 = extractSelectorLocations(
services,
styleContext.sourceAst,
);
const output = fs.readFileSync(outputFileName, "utf8");
assert.strictEqual(
`${JSON.stringify(locations, undefined, 2)}\n`,
output,
);
});
});
}
});
11 changes: 11 additions & 0 deletions tests/src/parser/test-utils.ts
Original file line number Diff line number Diff line change
@@ -480,6 +480,17 @@ export function styleContextToJson(styleContext: StyleContext): string {
}
}

export function selectorAstToJson(ast: any): string {
return JSON.stringify(ast, nodeReplacer, 2);

function nodeReplacer(key: string, value: any): any {
if (key === "parent" || key.startsWith("_")) {
return undefined;
}
return value;
}
}

function normalizeScope(scope: Scope | TSESScopes.Scope): any {
let variables = scope.variables as TSESScopes.Variable[];
if (scope.type === "global") {
67 changes: 65 additions & 2 deletions tools/update-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from "fs";
import path from "path";
import { Linter } from "eslint";
import type { Node } from "postcss";
import type { Root as SelectorRoot } from "postcss-selector-parser";
import * as parser from "../src/index.js";
import { parseForESLint } from "../src/parser/index.js";
import {
@@ -10,8 +12,10 @@ import {
astToJson,
normalizeError,
scopeToJSON,
selectorAstToJson,
styleContextToJson,
} from "../tests/src/parser/test-utils.js";
import { extractSelectorLocations } from "../tests/src/parser/style-selector-location-converter-utils.js";
import type ts from "typescript";
import type ESTree from "estree";
import globals from "globals";
@@ -27,10 +31,18 @@ const STYLE_CONTEXT_FIXTURE_ROOT = path.resolve(
dirname,
"../tests/fixtures/parser/style-context",
);
const STYLE_LOCATION_FIXTURE_ROOT = path.resolve(
const STYLE_LOCATION_CONVERTER_FIXTURE_ROOT = path.resolve(
dirname,
"../tests/fixtures/parser/style-location-converter",
);
const SELECTOR_PARSING_FIXTURE_ROOT = path.resolve(
dirname,
"../tests/fixtures/parser/selector-parsing",
);
const SELECTOR_CONVERTER_FIXTURE_ROOT = path.resolve(
dirname,
"../tests/fixtures/parser/style-selector-location-converter",
);

const RULES = [
"no-unused-labels",
@@ -165,7 +177,7 @@ for (const {
outputFileName,
config,
meetRequirements,
} of listupFixtures(STYLE_LOCATION_FIXTURE_ROOT)) {
} of listupFixtures(STYLE_LOCATION_CONVERTER_FIXTURE_ROOT)) {
if (!meetRequirements("parse")) {
continue;
}
@@ -176,16 +188,19 @@ for (const {
continue;
}
const locations: [
string,
Partial<SourceLocation>,
[number | undefined, number | undefined],
][] = [
[
"root",
services.styleNodeLoc(styleContext.sourceAst),
services.styleNodeRange(styleContext.sourceAst),
],
];
styleContext.sourceAst.walk((node) => {
locations.push([
node.type,
services.styleNodeLoc(node),
services.styleNodeRange(node),
]);
@@ -197,6 +212,54 @@ for (const {
);
}

for (const {
input,
inputFileName,
outputFileName,
config,
meetRequirements,
} of listupFixtures(SELECTOR_PARSING_FIXTURE_ROOT)) {
if (!meetRequirements("parse")) {
continue;
}
const services = parse(input, inputFileName, config).services;
const styleContext = services.getStyleContext();
const selectorASTs: SelectorRoot[] = [];
styleContext.sourceAst.walk((node: Node) => {
if (node.type === "rule") {
selectorASTs.push(services.getStyleSelectorAST(node));
}
});
fs.writeFileSync(
outputFileName,
`${selectorAstToJson(selectorASTs)}\n`,
"utf8",
);
}

for (const {
input,
inputFileName,
outputFileName,
config,
meetRequirements,
} of listupFixtures(SELECTOR_CONVERTER_FIXTURE_ROOT)) {
if (!meetRequirements("parse")) {
continue;
}
const services = parse(input, inputFileName, config).services;
const styleContext = services.getStyleContext();
if (styleContext.status !== "success") {
continue;
}
const locations = extractSelectorLocations(services, styleContext.sourceAst);
fs.writeFileSync(
outputFileName,
`${JSON.stringify(locations, undefined, 2)}\n`,
"utf8",
);
}

function buildTypes(
input: string,
result: {

Unchanged files with check annotations Beta

(key as any).parent = sAttr;
ctx.scriptLet.addObjectShorthandProperty(attribute.key, sAttr, (es) => {
if (
// FIXME: Older parsers may use the same node. In that case, do not replace.

Check warning on line 173 in src/parser/converts/attr.ts

GitHub Actions / lint

Unexpected 'fixme' comment: 'FIXME: Older parsers may use the same...'
// We will drop support for ESLint v7 in the next major version and remove this branch.
es.key !== es.value
) {
import type {} from "svelte"; // FIXME: Workaround to get type information for "svelte/compiler"

Check warning on line 1 in src/parser/template.ts

GitHub Actions / lint

Unexpected 'fixme' comment: 'FIXME: Workaround to get type...'
import { parse } from "svelte/compiler";
import type * as Compiler from "./svelte-ast-types-for-v5.js";
import type * as SvAST from "./svelte-ast-types.js";