Skip to content

Commit 159c69b

Browse files
authored
fix: support for reactive vars type information (#207)
* fix: wrong store access type information * Create grumpy-avocados-behave.md * test: update new fixture * test: ignore ts-eslint v4 * fix: support for reactive vars type information * Create tame-tigers-brush.md * test: ignore ts-eslint v4
1 parent 1c3901b commit 159c69b

33 files changed

+59826
-13
lines changed

.changeset/tame-tigers-brush.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": patch
3+
---
4+
5+
fix: support for reactive vars type information

src/context/script-let.ts

+11
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,17 @@ export class ScriptLetContext {
569569
}
570570
}
571571

572+
public appendDeclareReactiveVar(assignmentExpression: string): void {
573+
this.appendScriptWithoutOffset(
574+
`let ${assignmentExpression};`,
575+
(node, tokens, comments, result) => {
576+
tokens.length = 0;
577+
comments.length = 0;
578+
removeAllScope(node, result);
579+
}
580+
);
581+
}
582+
572583
private appendScript(
573584
text: string,
574585
offset: number,

src/parser/analyze-type/index.ts

+72-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,81 @@
1+
import type { ESLintExtendedProgram } from "..";
12
import type { Context } from "../../context";
3+
import { traverseNodes } from "../../traverse";
4+
import { parseScriptWithoutAnalyzeScope } from "../script";
25

36
/**
4-
* Append store type declarations.
7+
* Append type declarations for svelte variables.
8+
* - Append TypeScript code like
9+
* `declare let $foo: Parameters<Parameters<(typeof foo)["subscribe"]>[0]>[0];`
10+
* to define the type information for like `$foo` variable.
11+
* - Append TypeScript code like `let foo = bar;` to define the type information for like `$: foo = bar` variable.
12+
*/
13+
export function appendDeclareSvelteVarsTypes(ctx: Context): void {
14+
const vcode = ctx.sourceCode.scripts.vcode;
15+
16+
if (/\$\s*:\s*[\p{ID_Start}$(_]/u.test(vcode)) {
17+
// Probably have a reactive variable, so we will need to parse TypeScript once to extract the reactive variables.
18+
const result = parseScriptWithoutAnalyzeScope(
19+
vcode,
20+
ctx.sourceCode.scripts.attrs,
21+
{
22+
...ctx.parserOptions,
23+
// Without typings
24+
project: null,
25+
}
26+
);
27+
appendDeclareSvelteVarsTypesFromAST(result, vcode, ctx);
28+
} else {
29+
appendDeclareStoreTypesFromText(vcode, ctx);
30+
}
31+
}
32+
33+
/**
34+
* Append type declarations for svelte variables from AST.
35+
*/
36+
function appendDeclareSvelteVarsTypesFromAST(
37+
result: ESLintExtendedProgram,
38+
code: string,
39+
ctx: Context
40+
) {
41+
const maybeStores = new Set<string>();
42+
43+
traverseNodes(result.ast, {
44+
visitorKeys: result.visitorKeys,
45+
enterNode: (node, parent) => {
46+
if (node.type === "Identifier") {
47+
if (!node.name.startsWith("$") || node.name.length <= 1) {
48+
return;
49+
}
50+
maybeStores.add(node.name.slice(1));
51+
} else if (node.type === "LabeledStatement") {
52+
if (
53+
node.label.name !== "$" ||
54+
parent !== result.ast ||
55+
node.body.type !== "ExpressionStatement" ||
56+
node.body.expression.type !== "AssignmentExpression"
57+
) {
58+
return;
59+
}
60+
// It is reactive variable declaration.
61+
const text = code.slice(...node.body.expression.range!);
62+
ctx.scriptLet.appendDeclareReactiveVar(text);
63+
}
64+
},
65+
leaveNode() {
66+
/* noop */
67+
},
68+
});
69+
ctx.scriptLet.appendDeclareMaybeStores(maybeStores);
70+
}
71+
72+
/**
73+
* Append type declarations for store access.
574
* Append TypeScript code like
675
* `declare let $foo: Parameters<Parameters<(typeof foo)["subscribe"]>[0]>[0];`
7-
* to define the type information for like $foo variable.
76+
* to define the type information for like `$foo` variable.
877
*/
9-
export function appendDeclareStoreTypes(ctx: Context): void {
10-
const vcode = ctx.sourceCode.scripts.vcode;
78+
function appendDeclareStoreTypesFromText(vcode: string, ctx: Context): void {
1179
const extractStoreRe = /\$[\p{ID_Start}$_][\p{ID_Continue}$\u200c\u200d]*/giu;
1280
let m;
1381
const maybeStores = new Set<string>();

src/parser/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
analyzeStoreScope,
2020
} from "./analyze-scope";
2121
import { ParseError } from "../errors";
22-
import { appendDeclareStoreTypes } from "./analyze-type";
22+
import { appendDeclareSvelteVarsTypes } from "./analyze-type";
2323

2424
export interface ESLintProgram extends Program {
2525
comments: Comment[];
@@ -77,7 +77,7 @@ export function parseForESLint(
7777
parserOptions
7878
);
7979

80-
if (ctx.isTypeScript()) appendDeclareStoreTypes(ctx);
80+
if (ctx.isTypeScript()) appendDeclareSvelteVarsTypes(ctx);
8181

8282
const resultScript = parseScript(ctx.sourceCode.scripts, parserOptions);
8383
ctx.scriptLet.restore(resultScript);

src/parser/script.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function parseScript(
1212
script: ScriptsSourceCode,
1313
parserOptions: any = {}
1414
): ESLintExtendedProgram {
15-
const result = parseScriptWithoutAnalyzeScope(script, parserOptions);
15+
const result = parseScriptWithoutAnalyzeScopeFromVCode(script, parserOptions);
1616

1717
if (!result.scopeManager) {
1818
const scopeManager = analyzeScope(result.ast, parserOptions);
@@ -42,19 +42,31 @@ export function parseScript(
4242
/**
4343
* Parse for script without analyze scope
4444
*/
45-
function parseScriptWithoutAnalyzeScope(
46-
{ vcode, attrs }: ScriptsSourceCode,
45+
export function parseScriptWithoutAnalyzeScope(
46+
code: string,
47+
attrs: Record<string, string | undefined>,
4748
options: any
4849
): ESLintExtendedProgram {
4950
const parser = getParser(attrs, options.parser);
5051

5152
const result = isEnhancedParserObject(parser)
52-
? parser.parseForESLint(vcode, options)
53-
: parser.parse(vcode, options);
53+
? parser.parseForESLint(code, options)
54+
: parser.parse(code, options);
5455

5556
if ("ast" in result && result.ast != null) {
56-
result._virtualScriptCode = vcode;
5757
return result;
5858
}
59-
return { ast: result, _virtualScriptCode: vcode } as ESLintExtendedProgram;
59+
return { ast: result } as ESLintExtendedProgram;
60+
}
61+
62+
/**
63+
* Parse for script without analyze scope
64+
*/
65+
function parseScriptWithoutAnalyzeScopeFromVCode(
66+
{ vcode, attrs }: ScriptsSourceCode,
67+
options: any
68+
): ESLintExtendedProgram {
69+
const result = parseScriptWithoutAnalyzeScope(vcode, attrs, options);
70+
result._virtualScriptCode = vcode;
71+
return result;
6072
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
let x = "hello"
3+
const get = ()=>"hello"
4+
5+
$: y = x
6+
$: z = y
7+
$: foo = get
8+
</script>
9+
10+
<input title={z} bind:value={x}>
11+
{foo()}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
2+
import type { Linter } from "eslint";
3+
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
4+
import { rules } from "@typescript-eslint/eslint-plugin";
5+
export function setupLinter(linter: Linter) {
6+
linter.defineRule(
7+
"@typescript-eslint/no-unsafe-argument",
8+
rules["no-unsafe-argument"] as never
9+
);
10+
linter.defineRule(
11+
"@typescript-eslint/no-unsafe-assignment",
12+
rules["no-unsafe-assignment"] as never
13+
);
14+
linter.defineRule(
15+
"@typescript-eslint/no-unsafe-call",
16+
rules["no-unsafe-call"] as never
17+
);
18+
linter.defineRule(
19+
"@typescript-eslint/no-unsafe-member-access",
20+
rules["no-unsafe-member-access"] as never
21+
);
22+
linter.defineRule(
23+
"@typescript-eslint/no-unsafe-return",
24+
rules["no-unsafe-return"] as never
25+
);
26+
}
27+
28+
export function getConfig() {
29+
return {
30+
parser: "svelte-eslint-parser",
31+
parserOptions: BASIC_PARSER_OPTIONS,
32+
rules: {
33+
"@typescript-eslint/no-unsafe-argument": "error",
34+
"@typescript-eslint/no-unsafe-assignment": "error",
35+
"@typescript-eslint/no-unsafe-call": "error",
36+
"@typescript-eslint/no-unsafe-member-access": "error",
37+
"@typescript-eslint/no-unsafe-return": "error",
38+
},
39+
env: {
40+
browser: true,
41+
es2021: true,
42+
},
43+
};
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script lang="ts">
2+
// https://github.com/ota-meshi/svelte-eslint-parser/issues/206
3+
let obj = {
4+
child: {
5+
title: "hello!",
6+
},
7+
};
8+
9+
$: child = obj.child;
10+
$: title = child?.title ?? "Yo!";
11+
</script>
12+
13+
{child}{title}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
2+
import type { Linter } from "eslint";
3+
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
4+
import { rules } from "@typescript-eslint/eslint-plugin";
5+
export function setupLinter(linter: Linter) {
6+
linter.defineRule(
7+
"@typescript-eslint/no-unsafe-assignment",
8+
rules["no-unsafe-assignment"] as never
9+
);
10+
linter.defineRule(
11+
"@typescript-eslint/no-unsafe-member-access",
12+
rules["no-unsafe-member-access"] as never
13+
);
14+
}
15+
16+
export function getConfig() {
17+
return {
18+
parser: "svelte-eslint-parser",
19+
parserOptions: BASIC_PARSER_OPTIONS,
20+
rules: {
21+
"@typescript-eslint/no-unsafe-assignment": "error",
22+
"@typescript-eslint/no-unsafe-member-access": "error",
23+
},
24+
env: {
25+
browser: true,
26+
es2021: true,
27+
},
28+
};
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
let x = "hello"
3+
const get = ()=>"hello"
4+
5+
$: y = x
6+
$: z = y
7+
$: foo = get
8+
</script>
9+
10+
<input title={z} bind:value={x}>
11+
{foo()}

0 commit comments

Comments
 (0)