Skip to content

Commit

Permalink
fix: support for reactive vars type information (#207)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ota-meshi authored Aug 31, 2022
1 parent 1c3901b commit 159c69b
Show file tree
Hide file tree
Showing 33 changed files with 59,826 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-tigers-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": patch
---

fix: support for reactive vars type information
11 changes: 11 additions & 0 deletions src/context/script-let.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,17 @@ export class ScriptLetContext {
}
}

public appendDeclareReactiveVar(assignmentExpression: string): void {
this.appendScriptWithoutOffset(
`let ${assignmentExpression};`,
(node, tokens, comments, result) => {
tokens.length = 0;
comments.length = 0;
removeAllScope(node, result);
}
);
}

private appendScript(
text: string,
offset: number,
Expand Down
76 changes: 72 additions & 4 deletions src/parser/analyze-type/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,81 @@
import type { ESLintExtendedProgram } from "..";
import type { Context } from "../../context";
import { traverseNodes } from "../../traverse";
import { parseScriptWithoutAnalyzeScope } from "../script";

/**
* Append store type declarations.
* Append type declarations for svelte variables.
* - Append TypeScript code like
* `declare let $foo: Parameters<Parameters<(typeof foo)["subscribe"]>[0]>[0];`
* to define the type information for like `$foo` variable.
* - Append TypeScript code like `let foo = bar;` to define the type information for like `$: foo = bar` variable.
*/
export function appendDeclareSvelteVarsTypes(ctx: Context): void {
const vcode = ctx.sourceCode.scripts.vcode;

if (/\$\s*:\s*[\p{ID_Start}$(_]/u.test(vcode)) {
// Probably have a reactive variable, so we will need to parse TypeScript once to extract the reactive variables.
const result = parseScriptWithoutAnalyzeScope(
vcode,
ctx.sourceCode.scripts.attrs,
{
...ctx.parserOptions,
// Without typings
project: null,
}
);
appendDeclareSvelteVarsTypesFromAST(result, vcode, ctx);
} else {
appendDeclareStoreTypesFromText(vcode, ctx);
}
}

/**
* Append type declarations for svelte variables from AST.
*/
function appendDeclareSvelteVarsTypesFromAST(
result: ESLintExtendedProgram,
code: string,
ctx: Context
) {
const maybeStores = new Set<string>();

traverseNodes(result.ast, {
visitorKeys: result.visitorKeys,
enterNode: (node, parent) => {
if (node.type === "Identifier") {
if (!node.name.startsWith("$") || node.name.length <= 1) {
return;
}
maybeStores.add(node.name.slice(1));
} else if (node.type === "LabeledStatement") {
if (
node.label.name !== "$" ||
parent !== result.ast ||
node.body.type !== "ExpressionStatement" ||
node.body.expression.type !== "AssignmentExpression"
) {
return;
}
// It is reactive variable declaration.
const text = code.slice(...node.body.expression.range!);
ctx.scriptLet.appendDeclareReactiveVar(text);
}
},
leaveNode() {
/* noop */
},
});
ctx.scriptLet.appendDeclareMaybeStores(maybeStores);
}

/**
* Append type declarations for store access.
* Append TypeScript code like
* `declare let $foo: Parameters<Parameters<(typeof foo)["subscribe"]>[0]>[0];`
* to define the type information for like $foo variable.
* to define the type information for like `$foo` variable.
*/
export function appendDeclareStoreTypes(ctx: Context): void {
const vcode = ctx.sourceCode.scripts.vcode;
function appendDeclareStoreTypesFromText(vcode: string, ctx: Context): void {
const extractStoreRe = /\$[\p{ID_Start}$_][\p{ID_Continue}$\u200c\u200d]*/giu;
let m;
const maybeStores = new Set<string>();
Expand Down
4 changes: 2 additions & 2 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
analyzeStoreScope,
} from "./analyze-scope";
import { ParseError } from "../errors";
import { appendDeclareStoreTypes } from "./analyze-type";
import { appendDeclareSvelteVarsTypes } from "./analyze-type";

export interface ESLintProgram extends Program {
comments: Comment[];
Expand Down Expand Up @@ -77,7 +77,7 @@ export function parseForESLint(
parserOptions
);

if (ctx.isTypeScript()) appendDeclareStoreTypes(ctx);
if (ctx.isTypeScript()) appendDeclareSvelteVarsTypes(ctx);

const resultScript = parseScript(ctx.sourceCode.scripts, parserOptions);
ctx.scriptLet.restore(resultScript);
Expand Down
26 changes: 19 additions & 7 deletions src/parser/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function parseScript(
script: ScriptsSourceCode,
parserOptions: any = {}
): ESLintExtendedProgram {
const result = parseScriptWithoutAnalyzeScope(script, parserOptions);
const result = parseScriptWithoutAnalyzeScopeFromVCode(script, parserOptions);

if (!result.scopeManager) {
const scopeManager = analyzeScope(result.ast, parserOptions);
Expand Down Expand Up @@ -42,19 +42,31 @@ export function parseScript(
/**
* Parse for script without analyze scope
*/
function parseScriptWithoutAnalyzeScope(
{ vcode, attrs }: ScriptsSourceCode,
export function parseScriptWithoutAnalyzeScope(
code: string,
attrs: Record<string, string | undefined>,
options: any
): ESLintExtendedProgram {
const parser = getParser(attrs, options.parser);

const result = isEnhancedParserObject(parser)
? parser.parseForESLint(vcode, options)
: parser.parse(vcode, options);
? parser.parseForESLint(code, options)
: parser.parse(code, options);

if ("ast" in result && result.ast != null) {
result._virtualScriptCode = vcode;
return result;
}
return { ast: result, _virtualScriptCode: vcode } as ESLintExtendedProgram;
return { ast: result } as ESLintExtendedProgram;
}

/**
* Parse for script without analyze scope
*/
function parseScriptWithoutAnalyzeScopeFromVCode(
{ vcode, attrs }: ScriptsSourceCode,
options: any
): ESLintExtendedProgram {
const result = parseScriptWithoutAnalyzeScope(vcode, attrs, options);
result._virtualScriptCode = vcode;
return result;
}
11 changes: 11 additions & 0 deletions tests/fixtures/integrations/type-info-tests/reactive-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
let x = "hello"
const get = ()=>"hello"
$: y = x
$: z = y
$: foo = get
</script>

<input title={z} bind:value={x}>
{foo()}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
44 changes: 44 additions & 0 deletions tests/fixtures/integrations/type-info-tests/reactive-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
import type { Linter } from "eslint";
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
import { rules } from "@typescript-eslint/eslint-plugin";
export function setupLinter(linter: Linter) {
linter.defineRule(
"@typescript-eslint/no-unsafe-argument",
rules["no-unsafe-argument"] as never
);
linter.defineRule(
"@typescript-eslint/no-unsafe-assignment",
rules["no-unsafe-assignment"] as never
);
linter.defineRule(
"@typescript-eslint/no-unsafe-call",
rules["no-unsafe-call"] as never
);
linter.defineRule(
"@typescript-eslint/no-unsafe-member-access",
rules["no-unsafe-member-access"] as never
);
linter.defineRule(
"@typescript-eslint/no-unsafe-return",
rules["no-unsafe-return"] as never
);
}

export function getConfig() {
return {
parser: "svelte-eslint-parser",
parserOptions: BASIC_PARSER_OPTIONS,
rules: {
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error",
},
env: {
browser: true,
es2021: true,
},
};
}
13 changes: 13 additions & 0 deletions tests/fixtures/integrations/type-info-tests/reactive2-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
// https://github.com/ota-meshi/svelte-eslint-parser/issues/206
let obj = {
child: {
title: "hello!",
},
};
$: child = obj.child;
$: title = child?.title ?? "Yo!";
</script>

{child}{title}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
29 changes: 29 additions & 0 deletions tests/fixtures/integrations/type-info-tests/reactive2-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
import type { Linter } from "eslint";
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
import { rules } from "@typescript-eslint/eslint-plugin";
export function setupLinter(linter: Linter) {
linter.defineRule(
"@typescript-eslint/no-unsafe-assignment",
rules["no-unsafe-assignment"] as never
);
linter.defineRule(
"@typescript-eslint/no-unsafe-member-access",
rules["no-unsafe-member-access"] as never
);
}

export function getConfig() {
return {
parser: "svelte-eslint-parser",
parserOptions: BASIC_PARSER_OPTIONS,
rules: {
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
},
env: {
browser: true,
es2021: true,
},
};
}
11 changes: 11 additions & 0 deletions tests/fixtures/parser/ast/ts-reactive01-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
let x = "hello"
const get = ()=>"hello"
$: y = x
$: z = y
$: foo = get
</script>

<input title={z} bind:value={x}>
{foo()}
Loading

0 comments on commit 159c69b

Please sign in to comment.