Skip to content

Commit af55230

Browse files
authored
feat: improve $$ vars type (#338)
* feat: improve $$ vars type * Create flat-shrimps-type.md
1 parent 21c0dc6 commit af55230

33 files changed

+47280
-6
lines changed

.changeset/flat-shrimps-type.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: improve $$ vars type

src/context/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
Locations,
66
Position,
77
SvelteElement,
8+
SvelteHTMLElement,
89
SvelteName,
910
SvelteScriptElement,
1011
SvelteStyleElement,
@@ -116,6 +117,7 @@ export class Context {
116117

117118
public readonly parserOptions: any;
118119

120+
// ----- Source Code ------
119121
public readonly sourceCode: ContextSourceCode;
120122

121123
public readonly tokens: Token[] = [];
@@ -126,10 +128,14 @@ export class Context {
126128

127129
private readonly locsMap = new Map<number, Position>();
128130

131+
// ----- Context Data ------
129132
public readonly scriptLet: ScriptLetContext;
130133

131134
public readonly letDirCollections = new LetDirectiveCollections();
132135

136+
public readonly slots = new Set<SvelteHTMLElement>();
137+
138+
// ----- States ------
133139
private readonly state: { isTypeScript?: boolean } = {};
134140

135141
private readonly blocks: Block[] = [];

src/parser/converts/element.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,9 @@ function convertSlotElement(
707707
ctx: Context
708708
): SvelteHTMLElement {
709709
// Slot translates to SvelteHTMLElement.
710-
return convertHTMLElement(node, parent, ctx);
710+
const element = convertHTMLElement(node, parent, ctx);
711+
ctx.slots.add(element);
712+
return element;
711713
}
712714

713715
/** Convert for window element. e.g. <svelte:window> */

src/parser/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ export function parseForESLint(
8383
? parseTypeScript(
8484
scripts.getCurrentVirtualCodeInfo(),
8585
scripts.attrs,
86-
parserOptions
86+
parserOptions,
87+
{ slots: ctx.slots }
8788
)
8889
: parseScript(
8990
scripts.getCurrentVirtualCode(),

src/parser/typescript/analyze/index.ts

+107-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import { parseScriptWithoutAnalyzeScope } from "../../script";
1515
import { VirtualTypeScriptContext } from "../context";
1616
import type { TSESParseForESLintResult } from "../types";
1717
import type ESTree from "estree";
18+
import type { SvelteAttribute, SvelteHTMLElement } from "../../../ast";
19+
20+
export type AnalyzeTypeScriptContext = {
21+
slots: Set<SvelteHTMLElement>;
22+
};
1823

1924
const RESERVED_NAMES = new Set<string>(["$$props", "$$restProps", "$$slots"]);
2025
/**
@@ -25,7 +30,8 @@ const RESERVED_NAMES = new Set<string>(["$$props", "$$restProps", "$$slots"]);
2530
export function analyzeTypeScript(
2631
code: { script: string; render: string },
2732
attrs: Record<string, string | undefined>,
28-
parserOptions: any
33+
parserOptions: any,
34+
context: AnalyzeTypeScriptContext
2935
): VirtualTypeScriptContext {
3036
const ctx = new VirtualTypeScriptContext(code.script + code.render);
3137
ctx.appendOriginal(/^\s*/u.exec(code.script)![0].length);
@@ -44,6 +50,8 @@ export function analyzeTypeScript(
4450

4551
analyzeStoreReferenceNames(result, ctx);
4652

53+
analyzeDollarDollarVariables(result, ctx, context.slots);
54+
4755
analyzeReactiveScopes(result, ctx);
4856

4957
analyzeRenderScopes(code, ctx);
@@ -138,6 +146,104 @@ function analyzeStoreReferenceNames(
138146
}
139147
}
140148

149+
/**
150+
* Analyze `$$slots`, `$$props`, and `$$restProps` .
151+
* Insert type definitions code to provide correct type information for `$$slots`, `$$props`, and `$$restProps`.
152+
*/
153+
function analyzeDollarDollarVariables(
154+
result: TSESParseForESLintResult,
155+
ctx: VirtualTypeScriptContext,
156+
slots: Set<SvelteHTMLElement>
157+
) {
158+
const scopeManager = result.scopeManager;
159+
160+
if (
161+
scopeManager.globalScope!.through.some(
162+
(reference) => reference.identifier.name === "$$props"
163+
)
164+
) {
165+
appendDeclareVirtualScript("$$props", `{ [index: string]: any }`);
166+
}
167+
if (
168+
scopeManager.globalScope!.through.some(
169+
(reference) => reference.identifier.name === "$$restProps"
170+
)
171+
) {
172+
appendDeclareVirtualScript("$$restProps", `{ [index: string]: any }`);
173+
}
174+
if (
175+
scopeManager.globalScope!.through.some(
176+
(reference) => reference.identifier.name === "$$slots"
177+
)
178+
) {
179+
const nameTypes = new Set<string>();
180+
for (const slot of slots) {
181+
const nameAttr = slot.startTag.attributes.find(
182+
(attr): attr is SvelteAttribute =>
183+
attr.type === "SvelteAttribute" && attr.key.name === "name"
184+
);
185+
if (!nameAttr || nameAttr.value.length === 0) {
186+
nameTypes.add('"default"');
187+
continue;
188+
}
189+
190+
if (nameAttr.value.length === 1) {
191+
const value = nameAttr.value[0];
192+
if (value.type === "SvelteLiteral") {
193+
nameTypes.add(JSON.stringify(value.value));
194+
} else {
195+
nameTypes.add("string");
196+
}
197+
continue;
198+
}
199+
nameTypes.add(
200+
`\`${nameAttr.value
201+
.map((value) =>
202+
value.type === "SvelteLiteral"
203+
? value.value.replace(/([$`])/gu, "\\$1")
204+
: "${string}"
205+
)
206+
.join("")}\``
207+
);
208+
}
209+
210+
appendDeclareVirtualScript(
211+
"$$slots",
212+
`Record<${
213+
nameTypes.size > 0 ? [...nameTypes].join(" | ") : "any"
214+
}, boolean>`
215+
);
216+
}
217+
218+
/** Append declare virtual script */
219+
function appendDeclareVirtualScript(name: string, type: string) {
220+
ctx.appendVirtualScript(`declare let ${name}: ${type};`);
221+
ctx.restoreContext.addRestoreStatementProcess((node, result) => {
222+
if (
223+
node.type !== "VariableDeclaration" ||
224+
!node.declare ||
225+
node.declarations.length !== 1 ||
226+
node.declarations[0].id.type !== "Identifier" ||
227+
node.declarations[0].id.name !== name
228+
) {
229+
return false;
230+
}
231+
const program = result.ast;
232+
program.body.splice(program.body.indexOf(node), 1);
233+
234+
const scopeManager = result.scopeManager as ScopeManager;
235+
236+
// Remove `declare` variable
237+
removeAllScopeAndVariableAndReference(node, {
238+
visitorKeys: result.visitorKeys,
239+
scopeManager,
240+
});
241+
242+
return true;
243+
});
244+
}
245+
}
246+
141247
/**
142248
* Analyze the reactive scopes.
143249
* Transform source code to provide the correct type information in the `$:` statements.

src/parser/typescript/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ESLintExtendedProgram } from "..";
22
import { parseScript } from "../script";
3+
import type { AnalyzeTypeScriptContext } from "./analyze";
34
import { analyzeTypeScript } from "./analyze";
45
import type { TSESParseForESLintResult } from "./types";
56

@@ -9,9 +10,10 @@ import type { TSESParseForESLintResult } from "./types";
910
export function parseTypeScript(
1011
code: { script: string; render: string },
1112
attrs: Record<string, string | undefined>,
12-
parserOptions: any = {}
13+
parserOptions: unknown,
14+
context: AnalyzeTypeScriptContext
1315
): ESLintExtendedProgram {
14-
const tsCtx = analyzeTypeScript(code, attrs, parserOptions);
16+
const tsCtx = analyzeTypeScript(code, attrs, parserOptions, context);
1517

1618
const result = parseScript(tsCtx.script, attrs, parserOptions);
1719

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts">
2+
$$props
3+
$$restProps
4+
</script>
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"ruleId": "no-unused-expressions",
4+
"code": "$$props",
5+
"line": 2,
6+
"column": 5
7+
},
8+
{
9+
"ruleId": "no-unused-expressions",
10+
"code": "$$restProps",
11+
"line": 3,
12+
"column": 5
13+
}
14+
]

0 commit comments

Comments
 (0)