From 11e6e0014cc0883df9163a869bce653d9afa95fc Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 17 Nov 2023 12:07:39 +0900 Subject: [PATCH 1/4] test: add tests --- .../type-info-tests/$derived-input.svelte | 17 +++++++ .../type-info-tests/$derived-output.json | 9 ++++ .../type-info-tests/$derived-setup.ts | 47 +++++++++++++++++++ .../$derived-ts-input.svelte.ts | 12 +++++ .../type-info-tests/$derived-ts-output.json | 9 ++++ .../type-info-tests/$derived-ts-setup.ts | 47 +++++++++++++++++++ .../type-info-tests/$derived2-input.svelte | 15 ++++++ .../type-info-tests/$derived2-output.json | 9 ++++ .../type-info-tests/$derived2-setup.ts | 47 +++++++++++++++++++ .../$derived2-ts-input.svelte.ts | 18 +++++++ .../type-info-tests/$derived2-ts-output.json | 9 ++++ .../type-info-tests/$derived2-ts-setup.ts | 47 +++++++++++++++++++ tests/src/integrations.ts | 21 ++++++--- 13 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-input.svelte create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-output.json create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-setup.ts create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-ts-input.svelte.ts create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-ts-output.json create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-ts-setup.ts create mode 100644 tests/fixtures/integrations/type-info-tests/$derived2-input.svelte create mode 100644 tests/fixtures/integrations/type-info-tests/$derived2-output.json create mode 100644 tests/fixtures/integrations/type-info-tests/$derived2-setup.ts create mode 100644 tests/fixtures/integrations/type-info-tests/$derived2-ts-input.svelte.ts create mode 100644 tests/fixtures/integrations/type-info-tests/$derived2-ts-output.json create mode 100644 tests/fixtures/integrations/type-info-tests/$derived2-ts-setup.ts diff --git a/tests/fixtures/integrations/type-info-tests/$derived-input.svelte b/tests/fixtures/integrations/type-info-tests/$derived-input.svelte new file mode 100644 index 00000000..1799917a --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-input.svelte @@ -0,0 +1,17 @@ + + + +{foo()} diff --git a/tests/fixtures/integrations/type-info-tests/$derived-output.json b/tests/fixtures/integrations/type-info-tests/$derived-output.json new file mode 100644 index 00000000..033d80f1 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-output.json @@ -0,0 +1,9 @@ +[ + { + "ruleId": "@typescript-eslint/no-unsafe-argument", + "code": "y.foo", + "line": 8, + "column": 27, + "message": "Unsafe argument of type `any` assigned to a parameter of type `number`." + } +] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived-setup.ts b/tests/fixtures/integrations/type-info-tests/$derived-setup.ts new file mode 100644 index 00000000..d53704e5 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-setup.ts @@ -0,0 +1,47 @@ +/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */ +import type { Linter } from "eslint"; +import { generateParserOptions } 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: { + ...generateParserOptions(), + svelteFeatures: { runes: true }, + }, + 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, + }, + }; +} diff --git a/tests/fixtures/integrations/type-info-tests/$derived-ts-input.svelte.ts b/tests/fixtures/integrations/type-info-tests/$derived-ts-input.svelte.ts new file mode 100644 index 00000000..7b17de2c --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-ts-input.svelte.ts @@ -0,0 +1,12 @@ +type Info = { foo: number }; +let x: Info | null = { foo: 42 }; +const get = () => "hello"; + +x = null; +const y = $derived(x); +const z = $derived(fn(y.foo)); +const foo = $derived(get); + +function fn(a: number): number { + return a; +} diff --git a/tests/fixtures/integrations/type-info-tests/$derived-ts-output.json b/tests/fixtures/integrations/type-info-tests/$derived-ts-output.json new file mode 100644 index 00000000..d2061ed5 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-ts-output.json @@ -0,0 +1,9 @@ +[ + { + "ruleId": "@typescript-eslint/no-unsafe-argument", + "code": "y.foo", + "line": 7, + "column": 23, + "message": "Unsafe argument of type `any` assigned to a parameter of type `number`." + } +] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived-ts-setup.ts b/tests/fixtures/integrations/type-info-tests/$derived-ts-setup.ts new file mode 100644 index 00000000..d53704e5 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-ts-setup.ts @@ -0,0 +1,47 @@ +/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */ +import type { Linter } from "eslint"; +import { generateParserOptions } 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: { + ...generateParserOptions(), + svelteFeatures: { runes: true }, + }, + 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, + }, + }; +} diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-input.svelte b/tests/fixtures/integrations/type-info-tests/$derived2-input.svelte new file mode 100644 index 00000000..07693c79 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-input.svelte @@ -0,0 +1,15 @@ + + +{d} diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-output.json b/tests/fixtures/integrations/type-info-tests/$derived2-output.json new file mode 100644 index 00000000..cc653ec6 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-output.json @@ -0,0 +1,9 @@ +[ + { + "ruleId": "@typescript-eslint/no-unsafe-argument", + "code": "i.foo", + "line": 4, + "column": 41, + "message": "Unsafe argument of type `any` assigned to a parameter of type `number`." + } +] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-setup.ts b/tests/fixtures/integrations/type-info-tests/$derived2-setup.ts new file mode 100644 index 00000000..d53704e5 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-setup.ts @@ -0,0 +1,47 @@ +/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */ +import type { Linter } from "eslint"; +import { generateParserOptions } 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: { + ...generateParserOptions(), + svelteFeatures: { runes: true }, + }, + 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, + }, + }; +} diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-ts-input.svelte.ts b/tests/fixtures/integrations/type-info-tests/$derived2-ts-input.svelte.ts new file mode 100644 index 00000000..9bc7dbbb --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-ts-input.svelte.ts @@ -0,0 +1,18 @@ +export type Info = { n: number }; +export function foo() { + let a: Info | null = $state(null); + a = null; // * + const d = $derived(a?.n ? fn(a.n) : null); + return { + get d() { + return d; + }, + set(b: Info | null) { + a = b; + }, + }; + + function fn(n: number) { + return n * 2; + } +} diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-ts-output.json b/tests/fixtures/integrations/type-info-tests/$derived2-ts-output.json new file mode 100644 index 00000000..89822fe2 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-ts-output.json @@ -0,0 +1,9 @@ +[ + { + "ruleId": "@typescript-eslint/no-unsafe-argument", + "code": "a.n", + "line": 5, + "column": 32, + "message": "Unsafe argument of type `any` assigned to a parameter of type `number`." + } +] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-ts-setup.ts b/tests/fixtures/integrations/type-info-tests/$derived2-ts-setup.ts new file mode 100644 index 00000000..b1b1f599 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-ts-setup.ts @@ -0,0 +1,47 @@ +/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */ +import type { Linter } from "eslint"; +import { generateParserOptions } 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: { + ...generateParserOptions(), + svelteFeatures: { runes: true }, + }, + 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, + }, + }; +} diff --git a/tests/src/integrations.ts b/tests/src/integrations.ts index 23ce1992..4fee5605 100644 --- a/tests/src/integrations.ts +++ b/tests/src/integrations.ts @@ -9,6 +9,7 @@ import { listupFixtures, } from "./parser/test-utils"; import path from "path"; +import * as tsESLintParser from "@typescript-eslint/parser"; const FIXTURE_ROOT = path.resolve(__dirname, "../fixtures/integrations"); @@ -26,7 +27,7 @@ describe("Integration tests.", () => { )) { it(inputFileName, () => { const setupFileName = inputFileName.replace( - /input\.svelte$/u, + /input\.svelte(?:\.[jt]s)?$/u, "setup.ts", ); const setup = fs.existsSync(setupFileName) @@ -58,11 +59,19 @@ describe("Integration tests.", () => { 2, ); - if (fs.existsSync(outputFileName)) { - const output = fs.readFileSync(outputFileName, "utf8"); - assert.strictEqual(messagesJson, output); - } else { - fs.writeFileSync(outputFileName, messagesJson, "utf8"); + try { + if (fs.existsSync(outputFileName)) { + const output = fs.readFileSync(outputFileName, "utf8"); + assert.strictEqual(messagesJson, output); + } else { + fs.writeFileSync(outputFileName, messagesJson, "utf8"); + } + } finally { + // Clear type info cache + tsESLintParser.parseForESLint( + "", + generateParserOptions({ filePath: inputFileName }, config), + ); } }); } From 65a70e5cb3769d81469056b3cbf7c1159052edbe Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 17 Nov 2023 23:42:55 +0900 Subject: [PATCH 2/4] feat: apply correct type information to `$derived` argument expression --- src/parser/typescript/analyze/index.ts | 164 ++++++++++++++++-- src/parser/typescript/index.ts | 12 +- src/parser/typescript/restore.ts | 56 +++++- src/parser/typescript/set-parent.ts | 20 +++ .../type-info-tests/$derived-output.json | 10 +- .../type-info-tests/$derived-ts-output.json | 10 +- .../type-info-tests/$derived2-output.json | 10 +- .../type-info-tests/$derived2-ts-output.json | 10 +- 8 files changed, 232 insertions(+), 60 deletions(-) create mode 100644 src/parser/typescript/set-parent.ts diff --git a/src/parser/typescript/analyze/index.ts b/src/parser/typescript/analyze/index.ts index d46d0575..d1f25dee 100644 --- a/src/parser/typescript/analyze/index.ts +++ b/src/parser/typescript/analyze/index.ts @@ -18,11 +18,17 @@ import type ESTree from "estree"; import type { SvelteAttribute, SvelteHTMLElement } from "../../../ast"; import { globals, globalsForRunes } from "../../../parser/globals"; import type { NormalizedParserOptions } from "../../parser-options"; +import { setParent } from "../set-parent"; export type AnalyzeTypeScriptContext = { slots: Set; }; +type TransformInfo = { + node: TSESTree.Node; + transform: (ctx: VirtualTypeScriptContext) => void; +}; + /** * Analyze TypeScript source code in {a} -{b} -{c} +{b} +{c} {everythingElse} diff --git a/tests/fixtures/parser/ast/ts-use01-type-output.svelte b/tests/fixtures/parser/ast/ts-use01-type-output.svelte index 11d40e51..9aaf404e 100644 --- a/tests/fixtures/parser/ast/ts-use01-type-output.svelte +++ b/tests/fixtures/parser/ast/ts-use01-type-output.svelte @@ -1,6 +1,6 @@
{ // myAction: { (_node: HTMLElement, params: MyActionParam): { destroy: () => void; }; (_node: HTMLElement, params: MyActionParam): { ...; }; } + use:myAction={() => { // myAction: (_node: HTMLElement, params: MyActionParam) => { destroy: () => void; } return (param) => { // param: { foo: number; } param.foo; // param.foo: number }; diff --git a/tests/fixtures/tsconfig.test.json b/tests/fixtures/tsconfig.test.json index 86521018..e5167dc3 100644 --- a/tests/fixtures/tsconfig.test.json +++ b/tests/fixtures/tsconfig.test.json @@ -1,6 +1,7 @@ { "compilerOptions": { "strict": true, - "module": "commonjs" + "module": "Node16", + "moduleResolution": "Node16", } } \ No newline at end of file diff --git a/tests/src/integrations.ts b/tests/src/integrations.ts index 4fee5605..7112558b 100644 --- a/tests/src/integrations.ts +++ b/tests/src/integrations.ts @@ -9,7 +9,6 @@ import { listupFixtures, } from "./parser/test-utils"; import path from "path"; -import * as tsESLintParser from "@typescript-eslint/parser"; const FIXTURE_ROOT = path.resolve(__dirname, "../fixtures/integrations"); @@ -22,9 +21,16 @@ function createLinter() { } describe("Integration tests.", () => { - for (const { input, inputFileName, outputFileName, config } of listupFixtures( - FIXTURE_ROOT, - )) { + for (const { + input, + inputFileName, + outputFileName, + config, + meetRequirements, + } of listupFixtures(FIXTURE_ROOT)) { + if (!meetRequirements("parse")) { + continue; + } it(inputFileName, () => { const setupFileName = inputFileName.replace( /input\.svelte(?:\.[jt]s)?$/u, @@ -59,19 +65,11 @@ describe("Integration tests.", () => { 2, ); - try { - if (fs.existsSync(outputFileName)) { - const output = fs.readFileSync(outputFileName, "utf8"); - assert.strictEqual(messagesJson, output); - } else { - fs.writeFileSync(outputFileName, messagesJson, "utf8"); - } - } finally { - // Clear type info cache - tsESLintParser.parseForESLint( - "", - generateParserOptions({ filePath: inputFileName }, config), - ); + if (fs.existsSync(outputFileName)) { + const output = fs.readFileSync(outputFileName, "utf8"); + assert.strictEqual(messagesJson, output); + } else { + fs.writeFileSync(outputFileName, messagesJson, "utf8"); } }); } diff --git a/tools/update-fixtures.ts b/tools/update-fixtures.ts index 96c3340a..51d165ca 100644 --- a/tools/update-fixtures.ts +++ b/tools/update-fixtures.ts @@ -14,7 +14,6 @@ import { } from "../tests/src/parser/test-utils"; import type ts from "typescript"; import type ESTree from "estree"; -import * as tsESLintParser from "@typescript-eslint/parser"; import type { SourceLocation } from "../src/ast"; const ERROR_FIXTURE_ROOT = path.resolve( @@ -46,21 +45,10 @@ const RULES = [ "template-curly-spacing", ]; -let beforeFilePath: string | undefined; - /** * Parse */ function parse(code: string, filePath: string, config: any) { - if (beforeFilePath) { - // Clear type info cache - tsESLintParser.parseForESLint( - "", - generateParserOptions({ filePath: beforeFilePath }, config), - ); - } - - beforeFilePath = filePath; return parseForESLint(code, generateParserOptions({ filePath }, config)); } From 6953da6b4646450a3234a9d0a83d1ad9e2a6f26a Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 18 Nov 2023 00:10:02 +0900 Subject: [PATCH 4/4] Create blue-ghosts-tell.md --- .changeset/blue-ghosts-tell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-ghosts-tell.md diff --git a/.changeset/blue-ghosts-tell.md b/.changeset/blue-ghosts-tell.md new file mode 100644 index 00000000..ad023632 --- /dev/null +++ b/.changeset/blue-ghosts-tell.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": minor +--- + +feat: apply correct type information to `$derived` argument expression