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 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 + + +{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..0637a088 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived-requirements.json b/tests/fixtures/integrations/type-info-tests/$derived-requirements.json new file mode 100644 index 00000000..809a4e1f --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-requirements.json @@ -0,0 +1,5 @@ +{ + "parse": { + "svelte": ">=5.0.0-0" + } +} \ 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..0637a088 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-ts-output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived-ts-requirements.json b/tests/fixtures/integrations/type-info-tests/$derived-ts-requirements.json new file mode 100644 index 00000000..809a4e1f --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-ts-requirements.json @@ -0,0 +1,5 @@ +{ + "parse": { + "svelte": ">=5.0.0-0" + } +} \ 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..0637a088 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-requirements.json b/tests/fixtures/integrations/type-info-tests/$derived2-requirements.json new file mode 100644 index 00000000..809a4e1f --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-requirements.json @@ -0,0 +1,5 @@ +{ + "parse": { + "svelte": ">=5.0.0-0" + } +} \ 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..0637a088 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-ts-output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived2-ts-requirements.json b/tests/fixtures/integrations/type-info-tests/$derived2-ts-requirements.json new file mode 100644 index 00000000..809a4e1f --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived2-ts-requirements.json @@ -0,0 +1,5 @@ +{ + "parse": { + "svelte": ">=5.0.0-0" + } +} \ 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/fixtures/parser/ast/svelte5/ts-$props01-type-output.svelte b/tests/fixtures/parser/ast/svelte5/ts-$props01-type-output.svelte index feba88d5..619009a9 100644 --- a/tests/fixtures/parser/ast/svelte5/ts-$props01-type-output.svelte +++ b/tests/fixtures/parser/ast/svelte5/ts-$props01-type-output.svelte @@ -9,6 +9,6 @@ {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 23ce1992..7112558b 100644 --- a/tests/src/integrations.ts +++ b/tests/src/integrations.ts @@ -21,12 +21,19 @@ 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$/u, + /input\.svelte(?:\.[jt]s)?$/u, "setup.ts", ); const setup = fs.existsSync(setupFileName) 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)); }