Skip to content

Commit 1350734

Browse files
authored
fix: $derived argument expression to apply correct type information to this (#732)
1 parent c5f1d24 commit 1350734

File tree

6 files changed

+88
-14
lines changed

6 files changed

+88
-14
lines changed

.changeset/light-carrots-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": patch
3+
---
4+
5+
fix: `$derived` argument expression to apply correct type information to `this`

src/parser/typescript/analyze/index.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -946,17 +946,18 @@ function transformForReactiveStatement(
946946
}
947947

948948
/**
949-
* Transform for `$derived(expr)` to `$derived((()=>{ return fn(); function fn () { return expr } })())`
949+
* Transform for `$derived(expr)` to `$derived((()=>{ type This = typeof this; return fn(); function fn (this: This) { return expr } })())`
950950
*/
951951
function transformForDollarDerived(
952952
derivedCall: TSESTree.CallExpression,
953953
ctx: VirtualTypeScriptContext,
954954
) {
955955
const functionId = ctx.generateUniqueId("$derivedArgument");
956+
const thisTypeId = ctx.generateUniqueId("$This");
956957
const expression = derivedCall.arguments[0];
957958
ctx.appendOriginal(expression.range[0]);
958959
ctx.appendVirtualScript(
959-
`(()=>{return ${functionId}();function ${functionId}(){return `,
960+
`(()=>{type ${thisTypeId} = typeof this; return ${functionId}();function ${functionId}(this: ${thisTypeId}){return `,
960961
);
961962
ctx.appendOriginal(expression.range[1]);
962963
ctx.appendVirtualScript(`}})()`);
@@ -977,21 +978,40 @@ function transformForDollarDerived(
977978
arg.arguments.length !== 0 ||
978979
arg.callee.type !== "ArrowFunctionExpression" ||
979980
arg.callee.body.type !== "BlockStatement" ||
980-
arg.callee.body.body.length !== 2 ||
981-
arg.callee.body.body[0].type !== "ReturnStatement" ||
982-
arg.callee.body.body[0].argument?.type !== "CallExpression" ||
983-
arg.callee.body.body[0].argument.callee.type !== "Identifier" ||
984-
arg.callee.body.body[0].argument.callee.name !== functionId ||
985-
arg.callee.body.body[1].type !== "FunctionDeclaration" ||
986-
arg.callee.body.body[1].id.name !== functionId
981+
arg.callee.body.body.length !== 3
987982
) {
988983
return false;
989984
}
990-
const fnNode = arg.callee.body.body[1];
985+
const thisTypeNode = arg.callee.body.body[0];
991986
if (
987+
thisTypeNode.type !== "TSTypeAliasDeclaration" ||
988+
thisTypeNode.id.name !== thisTypeId
989+
) {
990+
return false;
991+
}
992+
const returnNode = arg.callee.body.body[1];
993+
if (
994+
returnNode.type !== "ReturnStatement" ||
995+
returnNode.argument?.type !== "CallExpression" ||
996+
returnNode.argument.callee.type !== "Identifier" ||
997+
returnNode.argument.callee.name !== functionId
998+
) {
999+
return false;
1000+
}
1001+
1002+
const fnNode = arg.callee.body.body[2];
1003+
if (
1004+
fnNode.type !== "FunctionDeclaration" ||
1005+
fnNode.id.name !== functionId ||
9921006
fnNode.body.body.length !== 1 ||
9931007
fnNode.body.body[0].type !== "ReturnStatement" ||
994-
!fnNode.body.body[0].argument
1008+
!fnNode.body.body[0].argument ||
1009+
fnNode.params[0]?.type !== "Identifier" ||
1010+
!fnNode.params[0].typeAnnotation ||
1011+
fnNode.params[0].typeAnnotation.typeAnnotation.type !==
1012+
"TSTypeReference" ||
1013+
fnNode.params[0].typeAnnotation.typeAnnotation.typeName.type !==
1014+
"Identifier"
9951015
) {
9961016
return false;
9971017
}
@@ -1002,11 +1022,16 @@ function transformForDollarDerived(
10021022
expr.parent = node;
10031023

10041024
const scopeManager = result.scopeManager as ScopeManager;
1005-
removeFunctionScope(arg.callee.body.body[1], scopeManager);
1025+
const fnScope = scopeManager.acquire(fnNode)!;
1026+
removeIdentifierVariable(fnNode.params[0], fnScope);
10061027
removeIdentifierReference(
1007-
arg.callee.body.body[0].argument.callee,
1008-
scopeManager.acquire(arg.callee)!,
1028+
fnNode.params[0].typeAnnotation.typeAnnotation.typeName,
1029+
fnScope,
10091030
);
1031+
removeFunctionScope(fnNode, scopeManager);
1032+
const scope = scopeManager.acquire(arg.callee)!;
1033+
removeIdentifierVariable(thisTypeNode.id, scope);
1034+
removeIdentifierReference(returnNode.argument.callee, scope);
10101035
removeFunctionScope(arg.callee, scopeManager);
10111036
return true;
10121037
},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class Product {
2+
x = $state(1)
3+
y = $state(2)
4+
result = $derived(this.x * this.y)
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"parse": {
3+
"svelte": ">=5.0.0-0"
4+
}
5+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Linter } from "eslint";
2+
import { generateParserOptions } from "../../../src/parser/test-utils.js";
3+
import { plugin } from "typescript-eslint";
4+
import * as parser from "../../../../src/index.js";
5+
import globals from "globals";
6+
7+
export function getConfig(): Linter.Config {
8+
return {
9+
plugins: {
10+
"@typescript-eslint": {
11+
rules: plugin.rules as any,
12+
},
13+
},
14+
languageOptions: {
15+
parser,
16+
parserOptions: {
17+
...generateParserOptions(),
18+
svelteFeatures: { runes: true },
19+
},
20+
globals: {
21+
...globals.browser,
22+
...globals.es2021,
23+
},
24+
},
25+
rules: {
26+
"@typescript-eslint/no-unsafe-argument": "error",
27+
"@typescript-eslint/no-unsafe-assignment": "error",
28+
"@typescript-eslint/no-unsafe-call": "error",
29+
"@typescript-eslint/no-unsafe-member-access": "error",
30+
"@typescript-eslint/no-unsafe-return": "error",
31+
},
32+
};
33+
}

0 commit comments

Comments
 (0)