Skip to content

Commit eddc3e3

Browse files
authored
fix: internal function hasTypeInfo misjudging and providing insufficient type information in complex cases. (#747)
1 parent ba1bf74 commit eddc3e3

File tree

3 files changed

+321
-14
lines changed

3 files changed

+321
-14
lines changed

.changeset/twelve-points-chew.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: internal function hasTypeInfo misjudging and providing insufficient type information in complex cases.

src/utils/index.ts

Lines changed: 108 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { TSESTree } from "@typescript-eslint/types";
2+
import type ESTree from "estree";
3+
14
/**
25
* Add element to a sorted array
36
*/
@@ -60,21 +63,112 @@ export function sortedLastIndex<T>(
6063
return upper;
6164
}
6265

63-
export function hasTypeInfo(element: any): boolean {
64-
if (element.type?.startsWith("TS")) {
65-
return true;
66-
}
67-
for (const key of Object.keys(element)) {
68-
if (key === "parent") continue;
69-
const value = element[key];
70-
if (value == null) continue;
71-
if (typeof value === "object") {
72-
if (hasTypeInfo(value)) return true;
73-
} else if (Array.isArray(value)) {
74-
for (const v of value) {
75-
if (typeof v === "object" && hasTypeInfo(v)) return true;
66+
/**
67+
* Checks if the given element has type information.
68+
*
69+
* Note: This function is not exhaustive and does not cover all possible cases.
70+
* However, it works sufficiently well for this parser.
71+
* @param element The element to check.
72+
* @returns True if the element has type information, false otherwise.
73+
*/
74+
export function hasTypeInfo(
75+
element: ESTree.Expression | TSESTree.Expression,
76+
): boolean {
77+
return isTypeInfoInternal(element as TSESTree.Expression);
78+
79+
function isTypeInfoInternal(
80+
node:
81+
| TSESTree.Expression
82+
| TSESTree.Parameter
83+
| TSESTree.Property
84+
| TSESTree.SpreadElement
85+
| TSESTree.TSEmptyBodyFunctionExpression,
86+
): boolean {
87+
// Handle expressions
88+
if (
89+
node.type.startsWith("TS") ||
90+
node.type === "Literal" ||
91+
node.type === "TemplateLiteral"
92+
) {
93+
return true;
94+
}
95+
if (
96+
node.type === "ArrowFunctionExpression" ||
97+
node.type === "FunctionExpression"
98+
) {
99+
if (node.params.some((param) => !isTypeInfoInternal(param))) return false;
100+
if (node.returnType) return true;
101+
if (node.body.type !== "BlockStatement") {
102+
// Check for type assertions in concise return expressions, e.g., `() => value as Type`
103+
return isTypeInfoInternal(node.body);
76104
}
105+
return false;
106+
}
107+
if (node.type === "ObjectExpression") {
108+
return node.properties.every((prop) => isTypeInfoInternal(prop));
109+
}
110+
if (node.type === "ArrayExpression") {
111+
return node.elements.every(
112+
(element) => element == null || isTypeInfoInternal(element),
113+
);
114+
}
115+
if (node.type === "UnaryExpression") {
116+
// All UnaryExpression operators always produce a value of a specific type regardless of the argument's type annotation:
117+
// - '!' : always boolean
118+
// - '+'/'-'/~: always number
119+
// - 'typeof' : always string (type name)
120+
// - 'void' : always undefined
121+
// - 'delete' : always boolean
122+
// Therefore, we always consider UnaryExpression as having type information.
123+
return true;
124+
}
125+
if (node.type === "UpdateExpression") {
126+
// All UpdateExpression operators ('++', '--') always produce a number value regardless of the argument's type annotation.
127+
// Therefore, we always consider UpdateExpression as having type information.
128+
return true;
129+
}
130+
if (node.type === "ConditionalExpression") {
131+
// ConditionalExpression (ternary) only has type information if both branches have type information.
132+
// e.g., a ? 1 : 2 → true (both are literals)
133+
// a ? 1 : b → false (alternate has no type info)
134+
// a ? b : c → false (neither has type info)
135+
return (
136+
isTypeInfoInternal(node.consequent) &&
137+
isTypeInfoInternal(node.alternate)
138+
);
139+
}
140+
if (node.type === "AssignmentExpression") {
141+
// AssignmentExpression only has type information if the right-hand side has type information.
142+
// e.g., a = 1 → true (right is literal)
143+
// a = b → false (right has no type info)
144+
return isTypeInfoInternal(node.right);
145+
}
146+
if (node.type === "SequenceExpression") {
147+
// SequenceExpression only has type information if the last expression has type information.
148+
// e.g., (a, b, 1) → true (last is literal)
149+
// (a, b, c) → false (last has no type info)
150+
if (node.expressions.length === 0) return false;
151+
return isTypeInfoInternal(node.expressions[node.expressions.length - 1]);
152+
}
153+
154+
// Handle destructuring and identifier patterns
155+
if (
156+
node.type === "Identifier" ||
157+
node.type === "ObjectPattern" ||
158+
node.type === "ArrayPattern" ||
159+
node.type === "AssignmentPattern" ||
160+
node.type === "RestElement"
161+
) {
162+
return Boolean(node.typeAnnotation);
163+
}
164+
165+
// Handle special nodes
166+
if (node.type === "SpreadElement") {
167+
return isTypeInfoInternal(node.argument);
168+
}
169+
if (node.type === "Property") {
170+
return isTypeInfoInternal(node.value);
77171
}
172+
return false;
78173
}
79-
return false;
80174
}

tests/src/utils/index.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import * as parser from "@typescript-eslint/parser";
2+
import assert from "assert";
3+
import * as utils from "../../../src/utils/index.js";
4+
5+
function parseExpression(input: string) {
6+
const ast = parser.parse(`async function * fn () { (${input}) }`);
7+
if (ast.body.length !== 1) {
8+
throw new Error("Expected a single expression");
9+
}
10+
const fn = ast.body[0];
11+
if (fn.type !== "FunctionDeclaration") {
12+
throw new Error("Expected an expression statement");
13+
}
14+
if (fn.body.body.length !== 1) {
15+
throw new Error("Expected a single expression in function body");
16+
}
17+
const body = fn.body.body[0];
18+
if (body.type !== "ExpressionStatement") {
19+
throw new Error("Expected an expression statement");
20+
}
21+
return body.expression;
22+
}
23+
24+
describe("hasTypeInfo (Expression)", () => {
25+
it("Identifier (no type)", () => {
26+
const node = parseExpression("a");
27+
assert.strictEqual(utils.hasTypeInfo(node), false);
28+
});
29+
it("Literal", () => {
30+
const node = parseExpression("42");
31+
assert.strictEqual(utils.hasTypeInfo(node), true);
32+
});
33+
34+
it("BinaryExpression (no type)", () => {
35+
const node = parseExpression("a + b");
36+
assert.strictEqual(utils.hasTypeInfo(node), false);
37+
});
38+
39+
it("ArrayExpression", () => {
40+
const node = parseExpression("[1, 2, 3]");
41+
assert.strictEqual(utils.hasTypeInfo(node), true);
42+
});
43+
44+
it("ObjectExpression", () => {
45+
const node = parseExpression("({a: 1})");
46+
assert.strictEqual(utils.hasTypeInfo(node), true);
47+
});
48+
49+
it("FunctionExpression (untyped param)", () => {
50+
const node = parseExpression("function(a) { return a }");
51+
assert.strictEqual(utils.hasTypeInfo(node), false);
52+
});
53+
54+
it("FunctionExpression (typed param)", () => {
55+
const node = parseExpression("function(a: number) { return a }");
56+
assert.strictEqual(utils.hasTypeInfo(node), false);
57+
});
58+
59+
it("FunctionExpression (typed return type)", () => {
60+
const node = parseExpression("function(): string { return '' }");
61+
assert.strictEqual(utils.hasTypeInfo(node), true);
62+
});
63+
64+
it("FunctionExpression (typed param and return type)", () => {
65+
const node = parseExpression("function(a: number): string { return '' }");
66+
assert.strictEqual(utils.hasTypeInfo(node), true);
67+
});
68+
69+
it("FunctionExpression (default param)", () => {
70+
const node = parseExpression("function(a = 1) { return a }");
71+
assert.strictEqual(utils.hasTypeInfo(node), false);
72+
});
73+
74+
it("FunctionExpression (rest param)", () => {
75+
const node = parseExpression("function(...args) { return args }");
76+
assert.strictEqual(utils.hasTypeInfo(node), false);
77+
});
78+
79+
it("FunctionExpression (type parameters)", () => {
80+
const node = parseExpression("function<T>(a: T) { return a }");
81+
assert.strictEqual(utils.hasTypeInfo(node), false);
82+
});
83+
84+
it("ArrowFunctionExpression (with typed param)", () => {
85+
const node = parseExpression("(a: string) => a");
86+
assert.strictEqual(utils.hasTypeInfo(node), false);
87+
});
88+
89+
it("ArrowFunctionExpression (untyped param)", () => {
90+
const node = parseExpression("(a) => a");
91+
assert.strictEqual(utils.hasTypeInfo(node), false);
92+
});
93+
94+
it("ArrowFunctionExpression (typed return type)", () => {
95+
const node = parseExpression("(): number => 1");
96+
assert.strictEqual(utils.hasTypeInfo(node), true);
97+
});
98+
99+
it("ArrowFunctionExpression (typed param and return type)", () => {
100+
const node = parseExpression("(a: string): number => 1");
101+
assert.strictEqual(utils.hasTypeInfo(node), true);
102+
});
103+
104+
it("ArrowFunctionExpression (default param)", () => {
105+
const node = parseExpression("(a = 1) => a");
106+
assert.strictEqual(utils.hasTypeInfo(node), false);
107+
});
108+
109+
it("ArrowFunctionExpression (rest param)", () => {
110+
const node = parseExpression("(...args) => args");
111+
assert.strictEqual(utils.hasTypeInfo(node), false);
112+
});
113+
114+
it("ArrowFunctionExpression (type parameters)", () => {
115+
const node = parseExpression("<T>(a: T) => a");
116+
assert.strictEqual(utils.hasTypeInfo(node), false);
117+
});
118+
119+
it("CallExpression (no type)", () => {
120+
const node = parseExpression("foo(1)");
121+
assert.strictEqual(utils.hasTypeInfo(node), false);
122+
});
123+
124+
it("MemberExpression (no type)", () => {
125+
const node = parseExpression("obj.prop");
126+
assert.strictEqual(utils.hasTypeInfo(node), false);
127+
});
128+
129+
it("TemplateLiteral", () => {
130+
const node = parseExpression("`hello ${a}`");
131+
assert.strictEqual(utils.hasTypeInfo(node), true);
132+
});
133+
134+
it("UnaryExpression", () => {
135+
const node = parseExpression("!a");
136+
assert.strictEqual(utils.hasTypeInfo(node), true);
137+
});
138+
139+
it("UpdateExpression", () => {
140+
const node = parseExpression("a++");
141+
assert.strictEqual(utils.hasTypeInfo(node), true);
142+
});
143+
144+
it("ConditionalExpression", () => {
145+
const node = parseExpression("a ? 1 : 2");
146+
assert.strictEqual(utils.hasTypeInfo(node), true);
147+
});
148+
149+
it("ConditionalExpression (no type)", () => {
150+
const node = parseExpression("a ? x : 2");
151+
assert.strictEqual(utils.hasTypeInfo(node), false);
152+
});
153+
154+
it("AssignmentExpression", () => {
155+
const node = parseExpression("a = 1");
156+
assert.strictEqual(utils.hasTypeInfo(node), true);
157+
});
158+
159+
it("AssignmentExpression (no type at right)", () => {
160+
const node = parseExpression("a = x");
161+
assert.strictEqual(utils.hasTypeInfo(node), false);
162+
});
163+
164+
it("SequenceExpression", () => {
165+
const node = parseExpression("(a, b, 1)");
166+
assert.strictEqual(utils.hasTypeInfo(node), true);
167+
});
168+
169+
it("SequenceExpression (no type at last)", () => {
170+
const node = parseExpression("(a, 1, b)");
171+
assert.strictEqual(utils.hasTypeInfo(node), false);
172+
});
173+
174+
it("NewExpression (no type)", () => {
175+
const node = parseExpression("new Foo(1)");
176+
assert.strictEqual(utils.hasTypeInfo(node), false);
177+
});
178+
179+
it("ClassExpression (no type)", () => {
180+
const node = parseExpression("class A {}");
181+
assert.strictEqual(utils.hasTypeInfo(node), false);
182+
});
183+
184+
it("TaggedTemplateExpression (no type)", () => {
185+
const node = parseExpression("tag`foo`");
186+
assert.strictEqual(utils.hasTypeInfo(node), false);
187+
});
188+
189+
it("AwaitExpression (no type)", () => {
190+
const node = parseExpression("await a");
191+
assert.strictEqual(utils.hasTypeInfo(node), false);
192+
});
193+
194+
it("YieldExpression (no type)", () => {
195+
const node = parseExpression("yield 1");
196+
assert.strictEqual(utils.hasTypeInfo(node), false);
197+
});
198+
199+
it("ImportExpression (no type)", () => {
200+
const node = parseExpression("import('foo')");
201+
assert.strictEqual(utils.hasTypeInfo(node), false);
202+
});
203+
204+
it("issue #746", () => {
205+
const node = parseExpression("e => {e; let b: number;}");
206+
assert.strictEqual(utils.hasTypeInfo(node), false);
207+
});
208+
});

0 commit comments

Comments
 (0)