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` diff --git a/src/parser/typescript/analyze/index.ts b/src/parser/typescript/analyze/index.ts index 4dde5837..ed13f0d7 100644 --- a/src/parser/typescript/analyze/index.ts +++ b/src/parser/typescript/analyze/index.ts @@ -946,17 +946,18 @@ 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, 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-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 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", + }, + }; +}