From 0681596215b71431d1af728182360324c70bea1d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Thu, 31 Jul 2025 15:18:28 +0900 Subject: [PATCH 1/4] fix: `$derived` argument expression to apply correct type information to `this` --- src/parser/typescript/analyze/index.ts | 51 ++++++++++++++----- .../$derived-within-class-input.svelte.ts | 5 ++ .../$derived-within-class-output.json | 1 + .../$derived-within-class-setup.ts | 33 ++++++++++++ 4 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-within-class-input.svelte.ts create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-within-class-output.json create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-within-class-setup.ts diff --git a/src/parser/typescript/analyze/index.ts b/src/parser/typescript/analyze/index.ts index 4dde5837..d6852254 100644 --- a/src/parser/typescript/analyze/index.ts +++ b/src/parser/typescript/analyze/index.ts @@ -953,10 +953,11 @@ function transformForDollarDerived( ctx: VirtualTypeScriptContext, ) { const functionId = ctx.generateUniqueId("$derivedArgument"); + const thisTypeId = ctx.generateUniqueId("$This"); const expression = derivedCall.arguments[0]; ctx.appendOriginal(expression.range[0]); ctx.appendVirtualScript( - `(()=>{return ${functionId}();function ${functionId}(){return `, + `(()=>{type ${thisTypeId} = typeof this; return ${functionId}();function ${functionId}(this: ${thisTypeId}){return `, ); ctx.appendOriginal(expression.range[1]); ctx.appendVirtualScript(`}})()`); @@ -977,21 +978,40 @@ function transformForDollarDerived( arg.arguments.length !== 0 || arg.callee.type !== "ArrowFunctionExpression" || arg.callee.body.type !== "BlockStatement" || - arg.callee.body.body.length !== 2 || - arg.callee.body.body[0].type !== "ReturnStatement" || - arg.callee.body.body[0].argument?.type !== "CallExpression" || - arg.callee.body.body[0].argument.callee.type !== "Identifier" || - arg.callee.body.body[0].argument.callee.name !== functionId || - arg.callee.body.body[1].type !== "FunctionDeclaration" || - arg.callee.body.body[1].id.name !== functionId + arg.callee.body.body.length !== 3 ) { return false; } - const fnNode = arg.callee.body.body[1]; + const thisTypeNode = arg.callee.body.body[0]; if ( + thisTypeNode.type !== "TSTypeAliasDeclaration" || + thisTypeNode.id.name !== thisTypeId + ) { + return false; + } + const returnNode = arg.callee.body.body[1]; + if ( + returnNode.type !== "ReturnStatement" || + returnNode.argument?.type !== "CallExpression" || + returnNode.argument.callee.type !== "Identifier" || + returnNode.argument.callee.name !== functionId + ) { + return false; + } + + const fnNode = arg.callee.body.body[2]; + if ( + fnNode.type !== "FunctionDeclaration" || + fnNode.id.name !== functionId || fnNode.body.body.length !== 1 || fnNode.body.body[0].type !== "ReturnStatement" || - !fnNode.body.body[0].argument + !fnNode.body.body[0].argument || + fnNode.params[0]?.type !== "Identifier" || + !fnNode.params[0].typeAnnotation || + fnNode.params[0].typeAnnotation.typeAnnotation.type !== + "TSTypeReference" || + fnNode.params[0].typeAnnotation.typeAnnotation.typeName.type !== + "Identifier" ) { return false; } @@ -1002,11 +1022,16 @@ function transformForDollarDerived( expr.parent = node; const scopeManager = result.scopeManager as ScopeManager; - removeFunctionScope(arg.callee.body.body[1], scopeManager); + const fnScope = scopeManager.acquire(fnNode)!; + removeIdentifierVariable(fnNode.params[0], fnScope); removeIdentifierReference( - arg.callee.body.body[0].argument.callee, - scopeManager.acquire(arg.callee)!, + fnNode.params[0].typeAnnotation.typeAnnotation.typeName, + fnScope, ); + removeFunctionScope(fnNode, scopeManager); + const scope = scopeManager.acquire(arg.callee)!; + removeIdentifierVariable(thisTypeNode.id, scope); + removeIdentifierReference(returnNode.argument.callee, scope); removeFunctionScope(arg.callee, scopeManager); return true; }, diff --git a/tests/fixtures/integrations/type-info-tests/$derived-within-class-input.svelte.ts b/tests/fixtures/integrations/type-info-tests/$derived-within-class-input.svelte.ts new file mode 100644 index 00000000..ff8e8c03 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-within-class-input.svelte.ts @@ -0,0 +1,5 @@ +export class Product { + x = $state(1) + y = $state(2) + result = $derived(this.x * this.y) +} \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived-within-class-output.json b/tests/fixtures/integrations/type-info-tests/$derived-within-class-output.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-within-class-output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/integrations/type-info-tests/$derived-within-class-setup.ts b/tests/fixtures/integrations/type-info-tests/$derived-within-class-setup.ts new file mode 100644 index 00000000..af932ff3 --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-within-class-setup.ts @@ -0,0 +1,33 @@ +import type { Linter } from "eslint"; +import { generateParserOptions } from "../../../src/parser/test-utils.js"; +import { plugin } from "typescript-eslint"; +import * as parser from "../../../../src/index.js"; +import globals from "globals"; + +export function getConfig(): Linter.Config { + return { + plugins: { + "@typescript-eslint": { + rules: plugin.rules as any, + }, + }, + languageOptions: { + parser, + parserOptions: { + ...generateParserOptions(), + svelteFeatures: { runes: true }, + }, + globals: { + ...globals.browser, + ...globals.es2021, + }, + }, + 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", + }, + }; +} From 5f23373c18618aeec9de7f46d625fd695073df8f Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Thu, 31 Jul 2025 15:22:14 +0900 Subject: [PATCH 2/4] fix --- .../type-info-tests/$derived-within-class-requirements.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/fixtures/integrations/type-info-tests/$derived-within-class-requirements.json diff --git a/tests/fixtures/integrations/type-info-tests/$derived-within-class-requirements.json b/tests/fixtures/integrations/type-info-tests/$derived-within-class-requirements.json new file mode 100644 index 00000000..809a4e1f --- /dev/null +++ b/tests/fixtures/integrations/type-info-tests/$derived-within-class-requirements.json @@ -0,0 +1,5 @@ +{ + "parse": { + "svelte": ">=5.0.0-0" + } +} \ No newline at end of file From 62302aeaba99c02f76bea11fd8b764ade3eeef13 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Thu, 31 Jul 2025 15:23:12 +0900 Subject: [PATCH 3/4] Create light-carrots-pump.md --- .changeset/light-carrots-pump.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/light-carrots-pump.md diff --git a/.changeset/light-carrots-pump.md b/.changeset/light-carrots-pump.md new file mode 100644 index 00000000..2715a12e --- /dev/null +++ b/.changeset/light-carrots-pump.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": patch +--- + +fix: `$derived` argument expression to apply correct type information to `this` From 5c8542e61f6414423c5c008a6e5417501e1a7bc3 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 2 Aug 2025 09:18:54 +0900 Subject: [PATCH 4/4] update comment --- src/parser/typescript/analyze/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/typescript/analyze/index.ts b/src/parser/typescript/analyze/index.ts index d6852254..ed13f0d7 100644 --- a/src/parser/typescript/analyze/index.ts +++ b/src/parser/typescript/analyze/index.ts @@ -946,7 +946,7 @@ function transformForReactiveStatement( } /** - * Transform for `$derived(expr)` to `$derived((()=>{ return fn(); function fn () { return expr } })())` + * Transform for `$derived(expr)` to `$derived((()=>{ type This = typeof this; return fn(); function fn (this: This) { return expr } })())` */ function transformForDollarDerived( derivedCall: TSESTree.CallExpression,