Skip to content
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/twelve-points-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": patch
---

fix: internal function hasTypeInfo misjudging and providing insufficient type information in complex cases.
122 changes: 108 additions & 14 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { TSESTree } from "@typescript-eslint/types";
import type ESTree from "estree";

/**
* Add element to a sorted array
*/
Expand Down Expand Up @@ -60,21 +63,112 @@ export function sortedLastIndex<T>(
return upper;
}

export function hasTypeInfo(element: any): boolean {
if (element.type?.startsWith("TS")) {
return true;
}
for (const key of Object.keys(element)) {
if (key === "parent") continue;
const value = element[key];
if (value == null) continue;
if (typeof value === "object") {
if (hasTypeInfo(value)) return true;
} else if (Array.isArray(value)) {
for (const v of value) {
if (typeof v === "object" && hasTypeInfo(v)) return true;
/**
* Checks if the given element has type information.
*
* Note: This function is not exhaustive and does not cover all possible cases.
* However, it works sufficiently well for this parser.
* @param element The element to check.
* @returns True if the element has type information, false otherwise.
*/
export function hasTypeInfo(
element: ESTree.Expression | TSESTree.Expression,
): boolean {
return isTypeInfoInternal(element as TSESTree.Expression);

function isTypeInfoInternal(
node:
| TSESTree.Expression
| TSESTree.Parameter
| TSESTree.Property
| TSESTree.SpreadElement
| TSESTree.TSEmptyBodyFunctionExpression,
): boolean {
// Handle expressions
if (
node.type.startsWith("TS") ||
node.type === "Literal" ||
node.type === "TemplateLiteral"
) {
return true;
}
if (
node.type === "ArrowFunctionExpression" ||
node.type === "FunctionExpression"
) {
if (node.params.some((param) => !isTypeInfoInternal(param))) return false;
if (node.returnType) return true;
if (node.body.type !== "BlockStatement") {
// Check for type assertions in concise return expressions, e.g., `() => value as Type`
return isTypeInfoInternal(node.body);
}
return false;
}
if (node.type === "ObjectExpression") {
return node.properties.every((prop) => isTypeInfoInternal(prop));
}
if (node.type === "ArrayExpression") {
return node.elements.every(
(element) => element == null || isTypeInfoInternal(element),
);
}
if (node.type === "UnaryExpression") {
// All UnaryExpression operators always produce a value of a specific type regardless of the argument's type annotation:
// - '!' : always boolean
// - '+'/'-'/~: always number
// - 'typeof' : always string (type name)
// - 'void' : always undefined
// - 'delete' : always boolean
// Therefore, we always consider UnaryExpression as having type information.
return true;
}
if (node.type === "UpdateExpression") {
// All UpdateExpression operators ('++', '--') always produce a number value regardless of the argument's type annotation.
// Therefore, we always consider UpdateExpression as having type information.
return true;
}
if (node.type === "ConditionalExpression") {
// ConditionalExpression (ternary) only has type information if both branches have type information.
// e.g., a ? 1 : 2 → true (both are literals)
// a ? 1 : b → false (alternate has no type info)
// a ? b : c → false (neither has type info)
return (
isTypeInfoInternal(node.consequent) &&
isTypeInfoInternal(node.alternate)
);
}
if (node.type === "AssignmentExpression") {
// AssignmentExpression only has type information if the right-hand side has type information.
// e.g., a = 1 → true (right is literal)
// a = b → false (right has no type info)
return isTypeInfoInternal(node.right);
}
if (node.type === "SequenceExpression") {
// SequenceExpression only has type information if the last expression has type information.
// e.g., (a, b, 1) → true (last is literal)
// (a, b, c) → false (last has no type info)
if (node.expressions.length === 0) return false;
return isTypeInfoInternal(node.expressions[node.expressions.length - 1]);
}

// Handle destructuring and identifier patterns
if (
node.type === "Identifier" ||
node.type === "ObjectPattern" ||
node.type === "ArrayPattern" ||
node.type === "AssignmentPattern" ||
node.type === "RestElement"
) {
return Boolean(node.typeAnnotation);
}

// Handle special nodes
if (node.type === "SpreadElement") {
return isTypeInfoInternal(node.argument);
}
if (node.type === "Property") {
return isTypeInfoInternal(node.value);
}
return false;
}
return false;
}
208 changes: 208 additions & 0 deletions tests/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import * as parser from "@typescript-eslint/parser";
import assert from "assert";
import * as utils from "../../../src/utils/index.js";

function parseExpression(input: string) {
const ast = parser.parse(`async function * fn () { (${input}) }`);
if (ast.body.length !== 1) {
throw new Error("Expected a single expression");
}
const fn = ast.body[0];
if (fn.type !== "FunctionDeclaration") {
throw new Error("Expected an expression statement");
}
if (fn.body.body.length !== 1) {
throw new Error("Expected a single expression in function body");
}
const body = fn.body.body[0];
if (body.type !== "ExpressionStatement") {
throw new Error("Expected an expression statement");
}
return body.expression;
}

describe("hasTypeInfo (Expression)", () => {
it("Identifier (no type)", () => {
const node = parseExpression("a");
assert.strictEqual(utils.hasTypeInfo(node), false);
});
it("Literal", () => {
const node = parseExpression("42");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("BinaryExpression (no type)", () => {
const node = parseExpression("a + b");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ArrayExpression", () => {
const node = parseExpression("[1, 2, 3]");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("ObjectExpression", () => {
const node = parseExpression("({a: 1})");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("FunctionExpression (untyped param)", () => {
const node = parseExpression("function(a) { return a }");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("FunctionExpression (typed param)", () => {
const node = parseExpression("function(a: number) { return a }");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("FunctionExpression (typed return type)", () => {
const node = parseExpression("function(): string { return '' }");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("FunctionExpression (typed param and return type)", () => {
const node = parseExpression("function(a: number): string { return '' }");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("FunctionExpression (default param)", () => {
const node = parseExpression("function(a = 1) { return a }");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("FunctionExpression (rest param)", () => {
const node = parseExpression("function(...args) { return args }");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("FunctionExpression (type parameters)", () => {
const node = parseExpression("function<T>(a: T) { return a }");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ArrowFunctionExpression (with typed param)", () => {
const node = parseExpression("(a: string) => a");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ArrowFunctionExpression (untyped param)", () => {
const node = parseExpression("(a) => a");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ArrowFunctionExpression (typed return type)", () => {
const node = parseExpression("(): number => 1");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("ArrowFunctionExpression (typed param and return type)", () => {
const node = parseExpression("(a: string): number => 1");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("ArrowFunctionExpression (default param)", () => {
const node = parseExpression("(a = 1) => a");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ArrowFunctionExpression (rest param)", () => {
const node = parseExpression("(...args) => args");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ArrowFunctionExpression (type parameters)", () => {
const node = parseExpression("<T>(a: T) => a");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("CallExpression (no type)", () => {
const node = parseExpression("foo(1)");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("MemberExpression (no type)", () => {
const node = parseExpression("obj.prop");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("TemplateLiteral", () => {
const node = parseExpression("`hello ${a}`");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("UnaryExpression", () => {
const node = parseExpression("!a");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("UpdateExpression", () => {
const node = parseExpression("a++");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("ConditionalExpression", () => {
const node = parseExpression("a ? 1 : 2");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("ConditionalExpression (no type)", () => {
const node = parseExpression("a ? x : 2");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("AssignmentExpression", () => {
const node = parseExpression("a = 1");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("AssignmentExpression (no type at right)", () => {
const node = parseExpression("a = x");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("SequenceExpression", () => {
const node = parseExpression("(a, b, 1)");
assert.strictEqual(utils.hasTypeInfo(node), true);
});

it("SequenceExpression (no type at last)", () => {
const node = parseExpression("(a, 1, b)");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("NewExpression (no type)", () => {
const node = parseExpression("new Foo(1)");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ClassExpression (no type)", () => {
const node = parseExpression("class A {}");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("TaggedTemplateExpression (no type)", () => {
const node = parseExpression("tag`foo`");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("AwaitExpression (no type)", () => {
const node = parseExpression("await a");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("YieldExpression (no type)", () => {
const node = parseExpression("yield 1");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("ImportExpression (no type)", () => {
const node = parseExpression("import('foo')");
assert.strictEqual(utils.hasTypeInfo(node), false);
});

it("issue #746", () => {
const node = parseExpression("e => {e; let b: number;}");
assert.strictEqual(utils.hasTypeInfo(node), false);
});
});
Loading