Skip to content

Commit 10ffeec

Browse files
authored
feat: add AST node for function bindings (#647)
1 parent 9ea27a5 commit 10ffeec

34 files changed

+5405
-31
lines changed

.changeset/red-pots-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: add AST node for function bindings

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ body:
2929
- type: textarea
3030
id: eslint-plugin-svelte-version
3131
attributes:
32-
label: What version of `eslint-plugin-svelte` and ` svelte-eslint-parser` are you using?
32+
label: What version of `eslint-plugin-svelte` and `svelte-eslint-parser` are you using?
3333
value: |
3434
- svelte-eslint-parser@0.0.0
3535
- eslint-plugin-svelte@0.0.0

docs/AST.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ interface SvelteBindingDirective extends Node {
278278
kind: "Binding";
279279
key: SvelteDirectiveKey;
280280
shorthand: boolean;
281-
expression: null | Expression;
281+
expression: null | Expression | SvelteFunctionBindingsExpression;
282282
}
283283
interface SvelteClassDirective extends Node {
284284
type: "SvelteDirective";
@@ -601,3 +601,20 @@ interface SvelteReactiveStatement extends Node {
601601
body: Statement;
602602
}
603603
```
604+
605+
### SvelteFunctionBindingsExpression
606+
607+
This node is a function bindings expression in `bind:name={get, set}`.\
608+
`SvelteFunctionBindingsExpression` is a special node to avoid confusing ESLint check rules with `SequenceExpression`.
609+
610+
```ts
611+
interface SvelteFunctionBindingsExpression extends Node {
612+
type: "SvelteFunctionBindingsExpression";
613+
expressions: [
614+
/** Getter */
615+
Expression,
616+
/** Setter */
617+
Expression,
618+
];
619+
}
620+
```

src/ast/html.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type ESTree from "estree";
22
import type { TSESTree } from "@typescript-eslint/types";
33
import type { BaseNode } from "./base.js";
44
import type { Token, Comment } from "./common.js";
5+
import type { SvelteFunctionBindingsExpression } from "./script.js";
56

67
export type SvelteHTMLNode =
78
| SvelteProgram
@@ -595,7 +596,7 @@ export interface SvelteBindingDirective extends BaseSvelteDirective {
595596
kind: "Binding";
596597
key: SvelteDirectiveKeyTextName;
597598
shorthand: boolean;
598-
expression: null | ESTree.Expression;
599+
expression: null | ESTree.Expression | SvelteFunctionBindingsExpression;
599600
}
600601
export interface SvelteClassDirective extends BaseSvelteDirective {
601602
kind: "Class";

src/ast/script.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type ESTree from "estree";
22
import type { BaseNode } from "./base.js";
33

4-
export type SvelteScriptNode = SvelteReactiveStatement;
4+
export type SvelteScriptNode =
5+
| SvelteReactiveStatement
6+
| SvelteFunctionBindingsExpression;
57

68
/** Node of `$` statement. */
79
export interface SvelteReactiveStatement extends BaseNode {
@@ -10,3 +12,14 @@ export interface SvelteReactiveStatement extends BaseNode {
1012
body: ESTree.Statement;
1113
parent: ESTree.Node;
1214
}
15+
16+
/** Node of `bind:name={get, set}` expression. */
17+
export interface SvelteFunctionBindingsExpression extends BaseNode {
18+
type: "SvelteFunctionBindingsExpression";
19+
expressions: [
20+
/** Getter */
21+
ESTree.Expression,
22+
/** Setter */
23+
ESTree.Expression,
24+
];
25+
}

src/context/script-let.ts

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,27 +79,72 @@ function getNodeRange(
7979
leadingComments?: Comment[];
8080
trailingComments?: Comment[];
8181
},
82+
code: string,
8283
): [number, number] {
83-
let start = null;
84-
let end = null;
84+
const loc =
85+
"range" in node
86+
? { start: node.range![0], end: node.range![1] }
87+
: getWithLoc(node);
88+
89+
let start = loc.start;
90+
let end = loc.end;
91+
92+
let openingParenCount = 0;
93+
let closingParenCount = 0;
8594
if (node.leadingComments) {
86-
start = getWithLoc(node.leadingComments[0]).start;
95+
const commentStart = getWithLoc(node.leadingComments[0]).start;
96+
if (commentStart < start) {
97+
start = commentStart;
98+
99+
// Extract the number of parentheses before the node.
100+
let leadingEnd = loc.start;
101+
for (let index = node.leadingComments.length - 1; index >= 0; index--) {
102+
const comment = node.leadingComments[index];
103+
const loc = getWithLoc(comment);
104+
for (const c of code.slice(loc.end, leadingEnd).trim()) {
105+
if (c === "(") openingParenCount++;
106+
}
107+
leadingEnd = loc.start;
108+
}
109+
}
87110
}
88111
if (node.trailingComments) {
89-
end = getWithLoc(
112+
const commentEnd = getWithLoc(
90113
node.trailingComments[node.trailingComments.length - 1],
91114
).end;
115+
if (end < commentEnd) {
116+
end = commentEnd;
117+
118+
// Extract the number of parentheses after the node.
119+
let trailingStart = loc.end;
120+
for (const comment of node.trailingComments) {
121+
const loc = getWithLoc(comment);
122+
for (const c of code.slice(trailingStart, loc.start).trim()) {
123+
if (c === ")") closingParenCount++;
124+
}
125+
trailingStart = loc.end;
126+
}
127+
}
92128
}
93129

94-
const loc =
95-
"range" in node
96-
? { start: node.range![0], end: node.range![1] }
97-
: getWithLoc(node);
130+
// Adjust the range so that the parentheses match up.
131+
if (openingParenCount < closingParenCount) {
132+
for (; openingParenCount < closingParenCount && start >= 0; start--) {
133+
const c = code[start].trim();
134+
if (c) continue;
135+
if (c !== "(") break;
136+
openingParenCount++;
137+
}
138+
} else if (openingParenCount > closingParenCount) {
139+
for (; openingParenCount > closingParenCount && end < code.length; end++) {
140+
const c = code[end].trim();
141+
if (c) continue;
142+
if (c !== ")") break;
143+
closingParenCount++;
144+
}
145+
}
98146

99-
return [
100-
start ? Math.min(start, loc.start) : loc.start,
101-
end ? Math.max(end, loc.end) : loc.end,
102-
];
147+
return [start, end];
103148
}
104149

105150
type StatementNodeType = `${TSESTree.Statement["type"]}`;
@@ -154,7 +199,7 @@ export class ScriptLetContext {
154199
typing?: string | null,
155200
...callbacks: ScriptLetCallback<E>[]
156201
): ScriptLetCallback<E>[] {
157-
const range = getNodeRange(expression);
202+
const range = getNodeRange(expression, this.ctx.code);
158203
return this.addExpressionFromRange(range, parent, typing, ...callbacks);
159204
}
160205

@@ -221,7 +266,7 @@ export class ScriptLetContext {
221266
parent: SvelteNode,
222267
...callbacks: ScriptLetCallback<ObjectShorthandProperty>[]
223268
): void {
224-
const range = getNodeRange(identifier);
269+
const range = getNodeRange(identifier, this.ctx.code);
225270
const part = this.ctx.code.slice(...range);
226271
this.appendScript(
227272
`({${part}});`,
@@ -260,8 +305,11 @@ export class ScriptLetContext {
260305
const range =
261306
declarator.type === "VariableDeclarator"
262307
? // As of Svelte v5-next.65, VariableDeclarator nodes do not have location information.
263-
[getNodeRange(declarator.id)[0], getNodeRange(declarator.init!)[1]]
264-
: getNodeRange(declarator);
308+
[
309+
getNodeRange(declarator.id, this.ctx.code)[0],
310+
getNodeRange(declarator.init!, this.ctx.code)[1],
311+
]
312+
: getNodeRange(declarator, this.ctx.code);
265313
const part = this.ctx.code.slice(...range);
266314
this.appendScript(
267315
`const ${part};`,
@@ -398,7 +446,7 @@ export class ScriptLetContext {
398446
ifBlock: SvelteIfBlock,
399447
callback: ScriptLetCallback<ESTree.Expression>,
400448
): void {
401-
const range = getNodeRange(expression);
449+
const range = getNodeRange(expression, this.ctx.code);
402450
const part = this.ctx.code.slice(...range);
403451
const restore = this.appendScript(
404452
`if(${part}){`,
@@ -442,8 +490,8 @@ export class ScriptLetContext {
442490
index: ESTree.Identifier | null,
443491
) => void,
444492
): void {
445-
const exprRange = getNodeRange(expression);
446-
const ctxRange = context && getNodeRange(context);
493+
const exprRange = getNodeRange(expression, this.ctx.code);
494+
const ctxRange = context && getNodeRange(context, this.ctx.code);
447495
let source = "Array.from(";
448496
const exprOffset = source.length;
449497
source += `${this.ctx.code.slice(...exprRange)}).forEach((`;
@@ -563,7 +611,7 @@ export class ScriptLetContext {
563611
callback: (id: ESTree.Identifier, params: ESTree.Pattern[]) => void,
564612
): void {
565613
const scopeKind = kind || this.currentScriptScopeKind;
566-
const idRange = getNodeRange(id);
614+
const idRange = getNodeRange(id, this.ctx.code);
567615
const part = this.ctx.code.slice(idRange[0], closeParentIndex + 1);
568616
const restore = this.appendScript(
569617
`function ${part}{`,
@@ -660,7 +708,7 @@ export class ScriptLetContext {
660708
.map((d) => {
661709
return {
662710
...d,
663-
range: getNodeRange(d.node),
711+
range: getNodeRange(d.node, this.ctx.code),
664712
};
665713
})
666714
.sort((a, b) => {

src/parser/converts/attr.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
SvelteStyleElement,
2121
SvelteElseBlock,
2222
SvelteAwaitBlock,
23+
SvelteFunctionBindingsExpression,
2324
} from "../../ast/index.js";
2425
import type ESTree from "estree";
2526
import type { Context } from "../../context/index.js";
@@ -367,6 +368,12 @@ function convertBindingDirective(
367368
null,
368369
(es, { getScope }) => {
369370
directive.expression = es;
371+
if (isFunctionBindings(ctx, es)) {
372+
(
373+
directive.expression as any as SvelteFunctionBindingsExpression
374+
).type = "SvelteFunctionBindingsExpression";
375+
return;
376+
}
370377
const scope = getScope(es);
371378
const reference = scope.references.find(
372379
(ref) => ref.identifier === es,
@@ -386,6 +393,34 @@ function convertBindingDirective(
386393
return directive;
387394
}
388395

396+
/**
397+
* Checks whether the given expression is Function bindings (added in Svelte 5.9.0) or not.
398+
* See https://svelte.dev/docs/svelte/bind#Function-bindings
399+
*/
400+
function isFunctionBindings(
401+
ctx: Context,
402+
expression: ESTree.Expression,
403+
): expression is ESTree.SequenceExpression {
404+
// Svelte 3/4 does not support Function bindings.
405+
if (!svelteVersion.gte(5)) {
406+
return false;
407+
}
408+
if (
409+
expression.type !== "SequenceExpression" ||
410+
expression.expressions.length !== 2
411+
) {
412+
return false;
413+
}
414+
const bindValueOpenIndex = ctx.code.lastIndexOf("{", expression.range![0]);
415+
if (bindValueOpenIndex < 0) return false;
416+
const betweenText = ctx.code
417+
.slice(bindValueOpenIndex + 1, expression.range![0])
418+
// Strip comments
419+
.replace(/\/\/[^\n]*\n|\/\*[\s\S]*?\*\//g, "")
420+
.trim();
421+
return !betweenText;
422+
}
423+
389424
/** Convert for EventHandler Directive */
390425
function convertEventHandlerDirective(
391426
node: SvAST.DirectiveForExpression | Compiler.OnDirective,
@@ -774,7 +809,10 @@ function buildLetDirectiveType(
774809
type DirectiveProcessors<
775810
D extends SvAST.Directive | StandardDirective,
776811
S extends SvelteDirective,
777-
E extends D["expression"] & S["expression"],
812+
E extends Exclude<
813+
D["expression"] & S["expression"],
814+
SvelteFunctionBindingsExpression
815+
>,
778816
> =
779817
| {
780818
processExpression: (
@@ -801,7 +839,10 @@ type DirectiveProcessors<
801839
function processDirective<
802840
D extends SvAST.Directive | StandardDirective,
803841
S extends SvelteDirective,
804-
E extends D["expression"] & S["expression"],
842+
E extends Exclude<
843+
D["expression"] & S["expression"],
844+
SvelteFunctionBindingsExpression
845+
>,
805846
>(
806847
node: D & { expression: null | E },
807848
directive: S,
@@ -878,7 +919,7 @@ function processDirectiveKey<
878919
function processDirectiveExpression<
879920
D extends SvAST.Directive | StandardDirective,
880921
S extends SvelteDirective,
881-
E extends D["expression"],
922+
E extends Exclude<D["expression"], SvelteFunctionBindingsExpression>,
882923
>(
883924
node: D & { expression: null | E },
884925
directive: S,
@@ -901,7 +942,12 @@ function processDirectiveExpression<
901942
}
902943
if (processors.processExpression) {
903944
processors.processExpression(node.expression, shorthand).push((es) => {
904-
if (node.expression && es.type !== node.expression.type) {
945+
if (
946+
node.expression &&
947+
((es.type as string) === "SvelteFunctionBindingsExpression"
948+
? "SequenceExpression"
949+
: es.type) !== node.expression.type
950+
) {
905951
throw new ParseError(
906952
`Expected ${node.expression.type}, but ${es.type} found.`,
907953
es.range![0],

src/visitor-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const svelteKeys: SvelteKeysType = {
5151
SvelteText: [],
5252
SvelteHTMLComment: [],
5353
SvelteReactiveStatement: ["label", "body"],
54+
SvelteFunctionBindingsExpression: ["expressions"],
5455
};
5556

5657
export const KEYS: SourceCode.VisitorKeys = unionWith(
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<input bind:value={
2+
() => value,
3+
(v) => value = v.toLowerCase()}
4+
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"ruleId": "no-undef",
4+
"code": "value",
5+
"line": 2,
6+
"column": 8
7+
},
8+
{
9+
"ruleId": "no-undef",
10+
"code": "value",
11+
"line": 3,
12+
"column": 9
13+
}
14+
]

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy