Skip to content

feat: improve $$ vars type #338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flat-shrimps-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": minor
---

feat: improve $$ vars type
6 changes: 6 additions & 0 deletions src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Locations,
Position,
SvelteElement,
SvelteHTMLElement,
SvelteName,
SvelteScriptElement,
SvelteStyleElement,
Expand Down Expand Up @@ -116,6 +117,7 @@ export class Context {

public readonly parserOptions: any;

// ----- Source Code ------
public readonly sourceCode: ContextSourceCode;

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

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

// ----- Context Data ------
public readonly scriptLet: ScriptLetContext;

public readonly letDirCollections = new LetDirectiveCollections();

public readonly slots = new Set<SvelteHTMLElement>();

// ----- States ------
private readonly state: { isTypeScript?: boolean } = {};

private readonly blocks: Block[] = [];
Expand Down
4 changes: 3 additions & 1 deletion src/parser/converts/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,9 @@ function convertSlotElement(
ctx: Context
): SvelteHTMLElement {
// Slot translates to SvelteHTMLElement.
return convertHTMLElement(node, parent, ctx);
const element = convertHTMLElement(node, parent, ctx);
ctx.slots.add(element);
return element;
}

/** Convert for window element. e.g. <svelte:window> */
Expand Down
3 changes: 2 additions & 1 deletion src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export function parseForESLint(
? parseTypeScript(
scripts.getCurrentVirtualCodeInfo(),
scripts.attrs,
parserOptions
parserOptions,
{ slots: ctx.slots }
)
: parseScript(
scripts.getCurrentVirtualCode(),
Expand Down
108 changes: 107 additions & 1 deletion src/parser/typescript/analyze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import { parseScriptWithoutAnalyzeScope } from "../../script";
import { VirtualTypeScriptContext } from "../context";
import type { TSESParseForESLintResult } from "../types";
import type ESTree from "estree";
import type { SvelteAttribute, SvelteHTMLElement } from "../../../ast";

export type AnalyzeTypeScriptContext = {
slots: Set<SvelteHTMLElement>;
};

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

analyzeStoreReferenceNames(result, ctx);

analyzeDollarDollarVariables(result, ctx, context.slots);

analyzeReactiveScopes(result, ctx);

analyzeRenderScopes(code, ctx);
Expand Down Expand Up @@ -138,6 +146,104 @@ function analyzeStoreReferenceNames(
}
}

/**
* Analyze `$$slots`, `$$props`, and `$$restProps` .
* Insert type definitions code to provide correct type information for `$$slots`, `$$props`, and `$$restProps`.
*/
function analyzeDollarDollarVariables(
result: TSESParseForESLintResult,
ctx: VirtualTypeScriptContext,
slots: Set<SvelteHTMLElement>
) {
const scopeManager = result.scopeManager;

if (
scopeManager.globalScope!.through.some(
(reference) => reference.identifier.name === "$$props"
)
) {
appendDeclareVirtualScript("$$props", `{ [index: string]: any }`);
}
if (
scopeManager.globalScope!.through.some(
(reference) => reference.identifier.name === "$$restProps"
)
) {
appendDeclareVirtualScript("$$restProps", `{ [index: string]: any }`);
}
if (
scopeManager.globalScope!.through.some(
(reference) => reference.identifier.name === "$$slots"
)
) {
const nameTypes = new Set<string>();
for (const slot of slots) {
const nameAttr = slot.startTag.attributes.find(
(attr): attr is SvelteAttribute =>
attr.type === "SvelteAttribute" && attr.key.name === "name"
);
if (!nameAttr || nameAttr.value.length === 0) {
nameTypes.add('"default"');
continue;
}

if (nameAttr.value.length === 1) {
const value = nameAttr.value[0];
if (value.type === "SvelteLiteral") {
nameTypes.add(JSON.stringify(value.value));
} else {
nameTypes.add("string");
}
continue;
}
nameTypes.add(
`\`${nameAttr.value
.map((value) =>
value.type === "SvelteLiteral"
? value.value.replace(/([$`])/gu, "\\$1")
: "${string}"
)
.join("")}\``
);
}

appendDeclareVirtualScript(
"$$slots",
`Record<${
nameTypes.size > 0 ? [...nameTypes].join(" | ") : "any"
}, boolean>`
);
}

/** Append declare virtual script */
function appendDeclareVirtualScript(name: string, type: string) {
ctx.appendVirtualScript(`declare let ${name}: ${type};`);
ctx.restoreContext.addRestoreStatementProcess((node, result) => {
if (
node.type !== "VariableDeclaration" ||
!node.declare ||
node.declarations.length !== 1 ||
node.declarations[0].id.type !== "Identifier" ||
node.declarations[0].id.name !== name
) {
return false;
}
const program = result.ast;
program.body.splice(program.body.indexOf(node), 1);

const scopeManager = result.scopeManager as ScopeManager;

// Remove `declare` variable
removeAllScopeAndVariableAndReference(node, {
visitorKeys: result.visitorKeys,
scopeManager,
});

return true;
});
}
}

/**
* Analyze the reactive scopes.
* Transform source code to provide the correct type information in the `$:` statements.
Expand Down
6 changes: 4 additions & 2 deletions src/parser/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ESLintExtendedProgram } from "..";
import { parseScript } from "../script";
import type { AnalyzeTypeScriptContext } from "./analyze";
import { analyzeTypeScript } from "./analyze";
import type { TSESParseForESLintResult } from "./types";

Expand All @@ -9,9 +10,10 @@ import type { TSESParseForESLintResult } from "./types";
export function parseTypeScript(
code: { script: string; render: string },
attrs: Record<string, string | undefined>,
parserOptions: any = {}
parserOptions: unknown,
context: AnalyzeTypeScriptContext
): ESLintExtendedProgram {
const tsCtx = analyzeTypeScript(code, attrs, parserOptions);
const tsCtx = analyzeTypeScript(code, attrs, parserOptions, context);

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

Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/parser/ast/ts-$$props01-input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
$$props
$$restProps
</script>

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"ruleId": "no-unused-expressions",
"code": "$$props",
"line": 2,
"column": 5
},
{
"ruleId": "no-unused-expressions",
"code": "$$restProps",
"line": 3,
"column": 5
}
]
Loading