diff --git a/.changeset/twelve-points-chew.md b/.changeset/twelve-points-chew.md new file mode 100644 index 00000000..b89afaed --- /dev/null +++ b/.changeset/twelve-points-chew.md @@ -0,0 +1,5 @@ +--- +"svelte-eslint-parser": patch +--- + +fix: internal function hasTypeInfo misjudging and providing insufficient type information in complex cases. diff --git a/src/utils/index.ts b/src/utils/index.ts index d54ff0f9..1a2ddf85 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,6 @@ +import type { TSESTree } from "@typescript-eslint/types"; +import type ESTree from "estree"; + /** * Add element to a sorted array */ @@ -60,21 +63,112 @@ export function sortedLastIndex( 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; } diff --git a/tests/src/utils/index.ts b/tests/src/utils/index.ts new file mode 100644 index 00000000..7cbc833f --- /dev/null +++ b/tests/src/utils/index.ts @@ -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(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("(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); + }); +});