From 744c6782a9652ac155f2a7b1d3f1997b7ff2c527 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sun, 9 Nov 2025 15:21:47 +0100 Subject: [PATCH 01/15] Chore: Extract AST check from convert.ts to ast-checks.ts --- packages/typescript-estree/src/ast-checks.ts | 54 ++++++++++++++++++++ packages/typescript-estree/src/convert.ts | 42 +-------------- 2 files changed, 56 insertions(+), 40 deletions(-) create mode 100644 packages/typescript-estree/src/ast-checks.ts diff --git a/packages/typescript-estree/src/ast-checks.ts b/packages/typescript-estree/src/ast-checks.ts new file mode 100644 index 000000000000..08115522dc97 --- /dev/null +++ b/packages/typescript-estree/src/ast-checks.ts @@ -0,0 +1,54 @@ +import * as ts from 'typescript'; + +import type { TSESTree } from './ts-estree'; + +import { isValidAssignmentTarget } from './node-utils'; + +export function checkForStatementDeclaration( + initializer: ts.ForInitializer, + kind: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, + throwError: ( + node: number | ts.Node | TSESTree.Range, + message: string, + ) => never, +): void { + const loop = kind === ts.SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; + if (ts.isVariableDeclarationList(initializer)) { + if (initializer.declarations.length !== 1) { + throwError( + initializer, + `Only a single variable declaration is allowed in a '${loop}' statement.`, + ); + } + const declaration = initializer.declarations[0]; + if (declaration.initializer) { + throwError( + declaration, + `The variable declaration of a '${loop}' statement cannot have an initializer.`, + ); + } else if (declaration.type) { + throwError( + declaration, + `The variable declaration of a '${loop}' statement cannot have a type annotation.`, + ); + } + if ( + kind === ts.SyntaxKind.ForInStatement && + initializer.flags & ts.NodeFlags.Using + ) { + throwError( + initializer, + "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", + ); + } + } else if ( + !isValidAssignmentTarget(initializer) && + initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression && + initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression + ) { + throwError( + initializer, + `The left-hand side of a '${loop}' statement must be a variable or a property access.`, + ); + } +} diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 3facbe34af20..782afdd62eed 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -10,6 +10,7 @@ import type { import type { SemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree, TSESTreeToTSNode, TSNode } from './ts-estree'; +import { checkForStatementDeclaration } from './ast-checks'; import { checkModifiers } from './check-modifiers'; import { getDecorators, getModifiers } from './getModifiers'; import { @@ -109,46 +110,7 @@ export class Converter { initializer: ts.ForInitializer, kind: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, ): void { - const loop = - kind === ts.SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; - if (ts.isVariableDeclarationList(initializer)) { - if (initializer.declarations.length !== 1) { - this.#throwError( - initializer, - `Only a single variable declaration is allowed in a '${loop}' statement.`, - ); - } - const declaration = initializer.declarations[0]; - if (declaration.initializer) { - this.#throwError( - declaration, - `The variable declaration of a '${loop}' statement cannot have an initializer.`, - ); - } else if (declaration.type) { - this.#throwError( - declaration, - `The variable declaration of a '${loop}' statement cannot have a type annotation.`, - ); - } - if ( - kind === ts.SyntaxKind.ForInStatement && - initializer.flags & ts.NodeFlags.Using - ) { - this.#throwError( - initializer, - "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", - ); - } - } else if ( - !isValidAssignmentTarget(initializer) && - initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression && - initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression - ) { - this.#throwError( - initializer, - `The left-hand side of a '${loop}' statement must be a variable or a property access.`, - ); - } + checkForStatementDeclaration(initializer, kind, this.#throwError); } #checkModifiers(node: ts.Node): void { From b52f21615ec523dc85181780c6ad7e7c3bea4f92 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sun, 9 Nov 2025 15:54:39 +0100 Subject: [PATCH 02/15] chore: extract AST check from convert.ts to ast-checks.ts From 9270fc40c363c587261193fedf8a13adc5de4baa Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sun, 9 Nov 2025 16:51:22 +0100 Subject: [PATCH 03/15] chore: add test for ast-checks.ts --- .../tests/lib/ast-checks.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/typescript-estree/tests/lib/ast-checks.test.ts diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts new file mode 100644 index 000000000000..691fb21f119f --- /dev/null +++ b/packages/typescript-estree/tests/lib/ast-checks.test.ts @@ -0,0 +1,49 @@ +import * as ts from 'typescript'; + +import type { TSESTree } from '../../src/ts-estree'; + +import { checkForStatementDeclaration } from '../../src/ast-checks'; + +describe(checkForStatementDeclaration, () => { + let throwError: ( + node: number | ts.Node | TSESTree.Range, + message: string, + ) => never; + + beforeEach(() => { + throwError = vi.fn((node, message: string) => { + throw new Error(message); + }) as unknown as typeof throwError; + }); + + it('allows single variable declaration with no initializer or type', () => { + const node = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('x'), + ]); + + expect(() => + checkForStatementDeclaration( + node, + ts.SyntaxKind.ForOfStatement, + throwError, + ), + ).not.toThrow(); + }); + + it('throws when multiple declarations', () => { + const node = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('x'), + ts.factory.createVariableDeclaration('y'), + ]); + + expect(() => + checkForStatementDeclaration( + node, + ts.SyntaxKind.ForOfStatement, + throwError, + ), + ).toThrow( + "Only a single variable declaration is allowed in a 'for...of' statement.", + ); + }); +}); From 6ced6b355ce01e748b1ef259f6ee9ba5e978f870 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sun, 9 Nov 2025 17:09:47 +0100 Subject: [PATCH 04/15] chore: add test for ast-checks.ts --- .../tests/lib/ast-checks.test.ts | 99 ++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts index 691fb21f119f..7aa76965bce1 100644 --- a/packages/typescript-estree/tests/lib/ast-checks.test.ts +++ b/packages/typescript-estree/tests/lib/ast-checks.test.ts @@ -1,7 +1,5 @@ import * as ts from 'typescript'; - import type { TSESTree } from '../../src/ts-estree'; - import { checkForStatementDeclaration } from '../../src/ast-checks'; describe(checkForStatementDeclaration, () => { @@ -46,4 +44,101 @@ describe(checkForStatementDeclaration, () => { "Only a single variable declaration is allowed in a 'for...of' statement.", ); }); + + it('throws when declaration has an initializer', () => { + const node = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration( + 'x', + undefined, + undefined, + ts.factory.createNumericLiteral(5), + ), + ]); + + expect(() => + checkForStatementDeclaration( + node, + ts.SyntaxKind.ForOfStatement, + throwError, + ), + ).toThrow( + "The variable declaration of a 'for...of' statement cannot have an initializer.", + ); + }); + + it('throws when declaration has a type annotation', () => { + const node = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration( + 'x', + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), + ), + ]); + + expect(() => + checkForStatementDeclaration( + node, + ts.SyntaxKind.ForOfStatement, + throwError, + ), + ).toThrow( + "The variable declaration of a 'for...of' statement cannot have a type annotation.", + ); + }); + + it('throws when "using" declaration in for...in', () => { + const node = ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration('x')], + ts.NodeFlags.Using, + ); + + expect(() => + checkForStatementDeclaration( + node, + ts.SyntaxKind.ForInStatement, + throwError, + ), + ).toThrow( + "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", + ); + }); + + it('throws when initializer is not a valid assignment target', () => { + const node = ts.factory.createNumericLiteral(42); + + expect(() => + checkForStatementDeclaration( + node, + ts.SyntaxKind.ForOfStatement, + throwError, + ), + ).toThrow( + "The left-hand side of a 'for...of' statement must be a variable or a property access.", + ); + }); + + it('handles both for...in and for...of paths', () => { + const forInNode = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('x'), + ]); + const forOfNode = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('x'), + ]); + + expect(() => + checkForStatementDeclaration( + forInNode, + ts.SyntaxKind.ForInStatement, + throwError, + ), + ).not.toThrow(); + + expect(() => + checkForStatementDeclaration( + forOfNode, + ts.SyntaxKind.ForOfStatement, + throwError, + ), + ).not.toThrow(); + }); }); From 65803e4127b9557ec6cf4802df12e588370d9930 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sun, 9 Nov 2025 17:21:36 +0100 Subject: [PATCH 05/15] chore: add test for ast-checks.ts --- packages/typescript-estree/tests/lib/ast-checks.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts index 7aa76965bce1..af37c92d7206 100644 --- a/packages/typescript-estree/tests/lib/ast-checks.test.ts +++ b/packages/typescript-estree/tests/lib/ast-checks.test.ts @@ -1,5 +1,7 @@ import * as ts from 'typescript'; + import type { TSESTree } from '../../src/ts-estree'; + import { checkForStatementDeclaration } from '../../src/ast-checks'; describe(checkForStatementDeclaration, () => { From b628bfa580ab3f2f42acf217fd9b8733d859e3ff Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sat, 15 Nov 2025 13:53:20 +0100 Subject: [PATCH 06/15] chore: add function in ast-checks to run ts node checks --- packages/typescript-estree/src/ast-checks.ts | 27 ++++++++++++++++++++ packages/typescript-estree/src/convert.ts | 22 +++++++--------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/typescript-estree/src/ast-checks.ts b/packages/typescript-estree/src/ast-checks.ts index 08115522dc97..7306a8528b30 100644 --- a/packages/typescript-estree/src/ast-checks.ts +++ b/packages/typescript-estree/src/ast-checks.ts @@ -4,6 +4,33 @@ import type { TSESTree } from './ts-estree'; import { isValidAssignmentTarget } from './node-utils'; +import { checkModifiers } from './check-modifiers'; + +export function checkTSNode( + node: ts.Node, + throwError: ( + node: number | ts.Node | TSESTree.Range, + message: string, + ) => never, + initializer?: ts.ForInitializer, + kind?: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, +): void { + checkModifiers(node); + + switch (node.kind) { + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: { + if (initializer && kind) { + checkForStatementDeclaration(initializer, kind, throwError); + } + break; + } + default: { + break; + } + } +} + export function checkForStatementDeclaration( initializer: ts.ForInitializer, kind: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 782afdd62eed..3f15917e891c 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -10,8 +10,7 @@ import type { import type { SemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree, TSESTreeToTSNode, TSNode } from './ts-estree'; -import { checkForStatementDeclaration } from './ast-checks'; -import { checkModifiers } from './check-modifiers'; +import { checkTSNode } from './ast-checks'; import { getDecorators, getModifiers } from './getModifiers'; import { canContainDirective, @@ -106,19 +105,16 @@ export class Converter { this.options = { ...options }; } - #checkForStatementDeclaration( - initializer: ts.ForInitializer, - kind: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, + #checkTSNode( + node: ts.Node, + initializer?: ts.ForInitializer, + kind?: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, ): void { - checkForStatementDeclaration(initializer, kind, this.#throwError); - } - - #checkModifiers(node: ts.Node): void { if (this.options.allowInvalidAST) { return; } - checkModifiers(node); + checkTSNode(node, this.#throwError, initializer, kind); } #throwError(node: number | ts.Node | TSESTree.Range, message: string): never { @@ -506,7 +502,7 @@ export class Converter { return null; } - this.#checkModifiers(node); + this.#checkTSNode(node); const pattern = this.allowPattern; if (allowPattern != null) { @@ -877,7 +873,7 @@ export class Converter { }); case SyntaxKind.ForInStatement: - this.#checkForStatementDeclaration(node.initializer, node.kind); + this.#checkTSNode(node, node.initializer, node.kind); return this.createNode(node, { type: AST_NODE_TYPES.ForInStatement, body: this.convertChild(node.statement), @@ -886,7 +882,7 @@ export class Converter { }); case SyntaxKind.ForOfStatement: { - this.#checkForStatementDeclaration(node.initializer, node.kind); + this.#checkTSNode(node, node.initializer, node.kind); return this.createNode(node, { type: AST_NODE_TYPES.ForOfStatement, await: Boolean( From 78f168682d52d378e63252b33b01233a8d24fb75 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sat, 15 Nov 2025 14:42:24 +0100 Subject: [PATCH 07/15] chore: fix lint errors --- packages/typescript-estree/src/ast-checks.ts | 3 +- .../tests/lib/ast-checks.test.ts | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/typescript-estree/src/ast-checks.ts b/packages/typescript-estree/src/ast-checks.ts index 7306a8528b30..9bdd88ba6b9b 100644 --- a/packages/typescript-estree/src/ast-checks.ts +++ b/packages/typescript-estree/src/ast-checks.ts @@ -2,9 +2,8 @@ import * as ts from 'typescript'; import type { TSESTree } from './ts-estree'; -import { isValidAssignmentTarget } from './node-utils'; - import { checkModifiers } from './check-modifiers'; +import { isValidAssignmentTarget } from './node-utils'; export function checkTSNode( node: ts.Node, diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts index af37c92d7206..2bb66a5276e9 100644 --- a/packages/typescript-estree/tests/lib/ast-checks.test.ts +++ b/packages/typescript-estree/tests/lib/ast-checks.test.ts @@ -2,7 +2,11 @@ import * as ts from 'typescript'; import type { TSESTree } from '../../src/ts-estree'; -import { checkForStatementDeclaration } from '../../src/ast-checks'; +import { + checkForStatementDeclaration, + checkTSNode, +} from '../../src/ast-checks'; +import { checkModifiers } from '../../src/check-modifiers'; describe(checkForStatementDeclaration, () => { let throwError: ( @@ -144,3 +148,43 @@ describe(checkForStatementDeclaration, () => { ).not.toThrow(); }); }); + +describe(checkTSNode, () => { + let throwError: ( + node: number | ts.Node | TSESTree.Range, + message: string, + ) => never; + + beforeEach(() => { + vi.clearAllMocks(); + throwError = vi.fn((node, message: string) => { + throw new Error(message); + }) as unknown as typeof throwError; + }); + + it('calls checkModifiers for a node', () => { + const node = ts.factory.createIdentifier('x'); + checkTSNode(node, throwError); + expect(checkModifiers).toHaveBeenCalledWith(node); + }); + + it('calls checkForStatementDeclaration for ForOfStatement', () => { + const initializer = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('x'), + ]); + const node = ts.factory.createForOfStatement( + undefined, + initializer, + ts.factory.createIdentifier('arr'), + ts.factory.createBlock([], true), + ); + + checkTSNode(node, throwError, initializer, ts.SyntaxKind.ForOfStatement); + + expect(checkForStatementDeclaration).toHaveBeenCalledWith( + initializer, + ts.SyntaxKind.ForOfStatement, + throwError, + ); + }); +}); From 001ea12c6768673d8a12800ff55f19414bcda831 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Sat, 15 Nov 2025 15:41:57 +0100 Subject: [PATCH 08/15] chore: update test --- .../tests/lib/ast-checks.test.ts | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts index 2bb66a5276e9..c4464295d6ad 100644 --- a/packages/typescript-estree/tests/lib/ast-checks.test.ts +++ b/packages/typescript-estree/tests/lib/ast-checks.test.ts @@ -6,7 +6,6 @@ import { checkForStatementDeclaration, checkTSNode, } from '../../src/ast-checks'; -import { checkModifiers } from '../../src/check-modifiers'; describe(checkForStatementDeclaration, () => { let throwError: ( @@ -162,16 +161,13 @@ describe(checkTSNode, () => { }) as unknown as typeof throwError; }); - it('calls checkModifiers for a node', () => { + it('does not throw for a normal identifier node', () => { const node = ts.factory.createIdentifier('x'); - checkTSNode(node, throwError); - expect(checkModifiers).toHaveBeenCalledWith(node); + expect(() => checkTSNode(node, throwError)).not.toThrow(); }); - it('calls checkForStatementDeclaration for ForOfStatement', () => { - const initializer = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('x'), - ]); + it('throws when ForOfStatement initializer is invalid', () => { + const initializer = ts.factory.createNumericLiteral(42); const node = ts.factory.createForOfStatement( undefined, initializer, @@ -179,12 +175,26 @@ describe(checkTSNode, () => { ts.factory.createBlock([], true), ); - checkTSNode(node, throwError, initializer, ts.SyntaxKind.ForOfStatement); + expect(() => + checkTSNode(node, throwError, initializer, ts.SyntaxKind.ForOfStatement), + ).toThrow( + "The left-hand side of a 'for...of' statement must be a variable or a property access.", + ); + }); - expect(checkForStatementDeclaration).toHaveBeenCalledWith( + it('does not throw for valid ForOfStatement initializer', () => { + const initializer = ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('x'), + ]); + const node = ts.factory.createForOfStatement( + undefined, initializer, - ts.SyntaxKind.ForOfStatement, - throwError, + ts.factory.createIdentifier('arr'), + ts.factory.createBlock([], true), ); + + expect(() => + checkTSNode(node, throwError, initializer, ts.SyntaxKind.ForOfStatement), + ).not.toThrow(); }); }); From f696ce1e5cc7f94f5bd392e45a3a7f001464b26b Mon Sep 17 00:00:00 2001 From: Lonercode Date: Wed, 19 Nov 2025 10:14:03 +0100 Subject: [PATCH 09/15] chore: address review --- packages/typescript-estree/src/ast-checks.ts | 4 ++-- packages/typescript-estree/src/convert.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/typescript-estree/src/ast-checks.ts b/packages/typescript-estree/src/ast-checks.ts index 9bdd88ba6b9b..ebd11a532a2e 100644 --- a/packages/typescript-estree/src/ast-checks.ts +++ b/packages/typescript-estree/src/ast-checks.ts @@ -12,7 +12,7 @@ export function checkTSNode( message: string, ) => never, initializer?: ts.ForInitializer, - kind?: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, + kind?: ts.SyntaxKind, ): void { checkModifiers(node); @@ -32,7 +32,7 @@ export function checkTSNode( export function checkForStatementDeclaration( initializer: ts.ForInitializer, - kind: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, + kind: ts.SyntaxKind, throwError: ( node: number | ts.Node | TSESTree.Range, message: string, diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 3f15917e891c..08f316d4cd77 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -108,7 +108,7 @@ export class Converter { #checkTSNode( node: ts.Node, initializer?: ts.ForInitializer, - kind?: ts.SyntaxKind.ForInStatement | ts.SyntaxKind.ForOfStatement, + kind?: ts.SyntaxKind, ): void { if (this.options.allowInvalidAST) { return; @@ -684,6 +684,12 @@ export class Converter { * @returns the converted ESTree node */ private convertNode(node: TSNode, parent: TSNode): TSESTree.Node | null { + this.#checkTSNode( + node, + (node as ts.ForInStatement | ts.ForOfStatement).initializer, + node.kind, + ); + switch (node.kind) { case SyntaxKind.SourceFile: { return this.createNode(node, { @@ -873,7 +879,6 @@ export class Converter { }); case SyntaxKind.ForInStatement: - this.#checkTSNode(node, node.initializer, node.kind); return this.createNode(node, { type: AST_NODE_TYPES.ForInStatement, body: this.convertChild(node.statement), @@ -882,7 +887,6 @@ export class Converter { }); case SyntaxKind.ForOfStatement: { - this.#checkTSNode(node, node.initializer, node.kind); return this.createNode(node, { type: AST_NODE_TYPES.ForOfStatement, await: Boolean( From 5eb48effa01ec8a08cb614c68d9a7b411c2d484b Mon Sep 17 00:00:00 2001 From: Lonercode Date: Wed, 19 Nov 2025 15:49:11 +0100 Subject: [PATCH 10/15] chore: review update --- packages/typescript-estree/src/ast-checks.ts | 10 +++++----- packages/typescript-estree/src/convert.ts | 14 ++------------ .../typescript-estree/tests/lib/ast-checks.test.ts | 8 ++------ 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/typescript-estree/src/ast-checks.ts b/packages/typescript-estree/src/ast-checks.ts index ebd11a532a2e..a1ea0fdeb599 100644 --- a/packages/typescript-estree/src/ast-checks.ts +++ b/packages/typescript-estree/src/ast-checks.ts @@ -11,17 +11,17 @@ export function checkTSNode( node: number | ts.Node | TSESTree.Range, message: string, ) => never, - initializer?: ts.ForInitializer, - kind?: ts.SyntaxKind, ): void { checkModifiers(node); switch (node.kind) { case ts.SyntaxKind.ForInStatement: case ts.SyntaxKind.ForOfStatement: { - if (initializer && kind) { - checkForStatementDeclaration(initializer, kind, throwError); - } + checkForStatementDeclaration( + (node as ts.ForInStatement | ts.ForOfStatement).initializer, + node.kind, + throwError, + ); break; } default: { diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 08f316d4cd77..191b98b47ca3 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -105,16 +105,12 @@ export class Converter { this.options = { ...options }; } - #checkTSNode( - node: ts.Node, - initializer?: ts.ForInitializer, - kind?: ts.SyntaxKind, - ): void { + #checkTSNode(node: ts.Node): void { if (this.options.allowInvalidAST) { return; } - checkTSNode(node, this.#throwError, initializer, kind); + checkTSNode(node, this.#throwError); } #throwError(node: number | ts.Node | TSESTree.Range, message: string): never { @@ -684,12 +680,6 @@ export class Converter { * @returns the converted ESTree node */ private convertNode(node: TSNode, parent: TSNode): TSESTree.Node | null { - this.#checkTSNode( - node, - (node as ts.ForInStatement | ts.ForOfStatement).initializer, - node.kind, - ); - switch (node.kind) { case SyntaxKind.SourceFile: { return this.createNode(node, { diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts index c4464295d6ad..77b672322441 100644 --- a/packages/typescript-estree/tests/lib/ast-checks.test.ts +++ b/packages/typescript-estree/tests/lib/ast-checks.test.ts @@ -175,9 +175,7 @@ describe(checkTSNode, () => { ts.factory.createBlock([], true), ); - expect(() => - checkTSNode(node, throwError, initializer, ts.SyntaxKind.ForOfStatement), - ).toThrow( + expect(() => checkTSNode(node, throwError)).toThrow( "The left-hand side of a 'for...of' statement must be a variable or a property access.", ); }); @@ -193,8 +191,6 @@ describe(checkTSNode, () => { ts.factory.createBlock([], true), ); - expect(() => - checkTSNode(node, throwError, initializer, ts.SyntaxKind.ForOfStatement), - ).not.toThrow(); + expect(() => checkTSNode(node, throwError)).not.toThrow(); }); }); From 37c500fb7924b3fd6b6e4ecd8f0bb30c575ecd11 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Thu, 27 Nov 2025 15:07:07 +0100 Subject: [PATCH 11/15] chore: update based on reviews --- .../src/check-syntax-errors.ts | 77 +++++++++++++++++++ packages/typescript-estree/src/convert.ts | 8 +- 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 packages/typescript-estree/src/check-syntax-errors.ts diff --git a/packages/typescript-estree/src/check-syntax-errors.ts b/packages/typescript-estree/src/check-syntax-errors.ts new file mode 100644 index 000000000000..098be8861d93 --- /dev/null +++ b/packages/typescript-estree/src/check-syntax-errors.ts @@ -0,0 +1,77 @@ +import * as ts from 'typescript'; + +import { checkModifiers } from './check-modifiers'; +import { isValidAssignmentTarget, createError } from './node-utils'; + +export function checkSyntaxError(node: ts.Node): void { + checkModifiers(node); + + switch (node.kind) { + case ts.SyntaxKind.ForInStatement: + case ts.SyntaxKind.ForOfStatement: { + checkForStatementDeclaration( + (node as ts.ForInStatement | ts.ForOfStatement).initializer, + node.kind, + ); + break; + } + default: { + break; + } + } +} + +function checkForStatementDeclaration( + initializer: ts.ForInitializer, + kind: ts.SyntaxKind, +): void { + const loop = kind === ts.SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; + if (ts.isVariableDeclarationList(initializer)) { + if (initializer.declarations.length !== 1) { + throw createError( + `Only a single variable declaration is allowed in a '${loop}' statement.`, + initializer.getSourceFile(), + initializer.getStart(), + initializer.getEnd(), + ); + } + const declaration = initializer.declarations[0]; + if (declaration.initializer) { + throw createError( + `The variable declaration of a '${loop}' statement cannot have an initializer.`, + declaration.getSourceFile(), + declaration.getStart(), + declaration.getEnd(), + ); + } else if (declaration.type) { + throw createError( + `The variable declaration of a '${loop}' statement cannot have a type annotation.`, + declaration.getSourceFile(), + declaration.getStart(), + declaration.getEnd(), + ); + } + if ( + kind === ts.SyntaxKind.ForInStatement && + initializer.flags & ts.NodeFlags.Using + ) { + throw createError( + "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", + initializer.getSourceFile(), + initializer.getStart(), + initializer.getEnd(), + ); + } + } else if ( + !isValidAssignmentTarget(initializer) && + initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression && + initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression + ) { + throw createError( + `The left-hand side of a '${loop}' statement must be a variable or a property access.`, + initializer.getSourceFile(), + initializer.getStart(), + initializer.getEnd(), + ); + } +} diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 191b98b47ca3..7c617f0c19ea 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -10,7 +10,7 @@ import type { import type { SemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree, TSESTreeToTSNode, TSNode } from './ts-estree'; -import { checkTSNode } from './ast-checks'; +import { checkSyntaxError } from './check-syntax-errors'; import { getDecorators, getModifiers } from './getModifiers'; import { canContainDirective, @@ -105,12 +105,12 @@ export class Converter { this.options = { ...options }; } - #checkTSNode(node: ts.Node): void { + #checkSyntaxError(node: ts.Node): void { if (this.options.allowInvalidAST) { return; } - checkTSNode(node, this.#throwError); + checkSyntaxError(node); } #throwError(node: number | ts.Node | TSESTree.Range, message: string): never { @@ -498,7 +498,7 @@ export class Converter { return null; } - this.#checkTSNode(node); + this.#checkSyntaxError(node); const pattern = this.allowPattern; if (allowPattern != null) { From 4e5902bb60a2b27bad41b13bba33e00a2e82c1ae Mon Sep 17 00:00:00 2001 From: Lonercode Date: Thu, 27 Nov 2025 15:29:29 +0100 Subject: [PATCH 12/15] chore: minor fix --- packages/typescript-estree/src/ast-checks.ts | 80 ------- .../tests/lib/ast-checks.test.ts | 196 ------------------ 2 files changed, 276 deletions(-) delete mode 100644 packages/typescript-estree/src/ast-checks.ts delete mode 100644 packages/typescript-estree/tests/lib/ast-checks.test.ts diff --git a/packages/typescript-estree/src/ast-checks.ts b/packages/typescript-estree/src/ast-checks.ts deleted file mode 100644 index a1ea0fdeb599..000000000000 --- a/packages/typescript-estree/src/ast-checks.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as ts from 'typescript'; - -import type { TSESTree } from './ts-estree'; - -import { checkModifiers } from './check-modifiers'; -import { isValidAssignmentTarget } from './node-utils'; - -export function checkTSNode( - node: ts.Node, - throwError: ( - node: number | ts.Node | TSESTree.Range, - message: string, - ) => never, -): void { - checkModifiers(node); - - switch (node.kind) { - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: { - checkForStatementDeclaration( - (node as ts.ForInStatement | ts.ForOfStatement).initializer, - node.kind, - throwError, - ); - break; - } - default: { - break; - } - } -} - -export function checkForStatementDeclaration( - initializer: ts.ForInitializer, - kind: ts.SyntaxKind, - throwError: ( - node: number | ts.Node | TSESTree.Range, - message: string, - ) => never, -): void { - const loop = kind === ts.SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; - if (ts.isVariableDeclarationList(initializer)) { - if (initializer.declarations.length !== 1) { - throwError( - initializer, - `Only a single variable declaration is allowed in a '${loop}' statement.`, - ); - } - const declaration = initializer.declarations[0]; - if (declaration.initializer) { - throwError( - declaration, - `The variable declaration of a '${loop}' statement cannot have an initializer.`, - ); - } else if (declaration.type) { - throwError( - declaration, - `The variable declaration of a '${loop}' statement cannot have a type annotation.`, - ); - } - if ( - kind === ts.SyntaxKind.ForInStatement && - initializer.flags & ts.NodeFlags.Using - ) { - throwError( - initializer, - "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", - ); - } - } else if ( - !isValidAssignmentTarget(initializer) && - initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression && - initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression - ) { - throwError( - initializer, - `The left-hand side of a '${loop}' statement must be a variable or a property access.`, - ); - } -} diff --git a/packages/typescript-estree/tests/lib/ast-checks.test.ts b/packages/typescript-estree/tests/lib/ast-checks.test.ts deleted file mode 100644 index 77b672322441..000000000000 --- a/packages/typescript-estree/tests/lib/ast-checks.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import * as ts from 'typescript'; - -import type { TSESTree } from '../../src/ts-estree'; - -import { - checkForStatementDeclaration, - checkTSNode, -} from '../../src/ast-checks'; - -describe(checkForStatementDeclaration, () => { - let throwError: ( - node: number | ts.Node | TSESTree.Range, - message: string, - ) => never; - - beforeEach(() => { - throwError = vi.fn((node, message: string) => { - throw new Error(message); - }) as unknown as typeof throwError; - }); - - it('allows single variable declaration with no initializer or type', () => { - const node = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('x'), - ]); - - expect(() => - checkForStatementDeclaration( - node, - ts.SyntaxKind.ForOfStatement, - throwError, - ), - ).not.toThrow(); - }); - - it('throws when multiple declarations', () => { - const node = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('x'), - ts.factory.createVariableDeclaration('y'), - ]); - - expect(() => - checkForStatementDeclaration( - node, - ts.SyntaxKind.ForOfStatement, - throwError, - ), - ).toThrow( - "Only a single variable declaration is allowed in a 'for...of' statement.", - ); - }); - - it('throws when declaration has an initializer', () => { - const node = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration( - 'x', - undefined, - undefined, - ts.factory.createNumericLiteral(5), - ), - ]); - - expect(() => - checkForStatementDeclaration( - node, - ts.SyntaxKind.ForOfStatement, - throwError, - ), - ).toThrow( - "The variable declaration of a 'for...of' statement cannot have an initializer.", - ); - }); - - it('throws when declaration has a type annotation', () => { - const node = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration( - 'x', - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), - ), - ]); - - expect(() => - checkForStatementDeclaration( - node, - ts.SyntaxKind.ForOfStatement, - throwError, - ), - ).toThrow( - "The variable declaration of a 'for...of' statement cannot have a type annotation.", - ); - }); - - it('throws when "using" declaration in for...in', () => { - const node = ts.factory.createVariableDeclarationList( - [ts.factory.createVariableDeclaration('x')], - ts.NodeFlags.Using, - ); - - expect(() => - checkForStatementDeclaration( - node, - ts.SyntaxKind.ForInStatement, - throwError, - ), - ).toThrow( - "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", - ); - }); - - it('throws when initializer is not a valid assignment target', () => { - const node = ts.factory.createNumericLiteral(42); - - expect(() => - checkForStatementDeclaration( - node, - ts.SyntaxKind.ForOfStatement, - throwError, - ), - ).toThrow( - "The left-hand side of a 'for...of' statement must be a variable or a property access.", - ); - }); - - it('handles both for...in and for...of paths', () => { - const forInNode = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('x'), - ]); - const forOfNode = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('x'), - ]); - - expect(() => - checkForStatementDeclaration( - forInNode, - ts.SyntaxKind.ForInStatement, - throwError, - ), - ).not.toThrow(); - - expect(() => - checkForStatementDeclaration( - forOfNode, - ts.SyntaxKind.ForOfStatement, - throwError, - ), - ).not.toThrow(); - }); -}); - -describe(checkTSNode, () => { - let throwError: ( - node: number | ts.Node | TSESTree.Range, - message: string, - ) => never; - - beforeEach(() => { - vi.clearAllMocks(); - throwError = vi.fn((node, message: string) => { - throw new Error(message); - }) as unknown as typeof throwError; - }); - - it('does not throw for a normal identifier node', () => { - const node = ts.factory.createIdentifier('x'); - expect(() => checkTSNode(node, throwError)).not.toThrow(); - }); - - it('throws when ForOfStatement initializer is invalid', () => { - const initializer = ts.factory.createNumericLiteral(42); - const node = ts.factory.createForOfStatement( - undefined, - initializer, - ts.factory.createIdentifier('arr'), - ts.factory.createBlock([], true), - ); - - expect(() => checkTSNode(node, throwError)).toThrow( - "The left-hand side of a 'for...of' statement must be a variable or a property access.", - ); - }); - - it('does not throw for valid ForOfStatement initializer', () => { - const initializer = ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('x'), - ]); - const node = ts.factory.createForOfStatement( - undefined, - initializer, - ts.factory.createIdentifier('arr'), - ts.factory.createBlock([], true), - ); - - expect(() => checkTSNode(node, throwError)).not.toThrow(); - }); -}); From 336c584ff6093b1e3ee06bab4d7c0f96046b77a2 Mon Sep 17 00:00:00 2001 From: Lonercode Date: Thu, 27 Nov 2025 15:58:02 +0100 Subject: [PATCH 13/15] chore: merge main and update code --- .../src/check-syntax-errors.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/typescript-estree/src/check-syntax-errors.ts b/packages/typescript-estree/src/check-syntax-errors.ts index 098be8861d93..048b18f80d4a 100644 --- a/packages/typescript-estree/src/check-syntax-errors.ts +++ b/packages/typescript-estree/src/check-syntax-errors.ts @@ -29,26 +29,20 @@ function checkForStatementDeclaration( if (ts.isVariableDeclarationList(initializer)) { if (initializer.declarations.length !== 1) { throw createError( + initializer, `Only a single variable declaration is allowed in a '${loop}' statement.`, - initializer.getSourceFile(), - initializer.getStart(), - initializer.getEnd(), ); } const declaration = initializer.declarations[0]; if (declaration.initializer) { throw createError( + declaration, `The variable declaration of a '${loop}' statement cannot have an initializer.`, - declaration.getSourceFile(), - declaration.getStart(), - declaration.getEnd(), ); } else if (declaration.type) { throw createError( + declaration, `The variable declaration of a '${loop}' statement cannot have a type annotation.`, - declaration.getSourceFile(), - declaration.getStart(), - declaration.getEnd(), ); } if ( @@ -56,10 +50,8 @@ function checkForStatementDeclaration( initializer.flags & ts.NodeFlags.Using ) { throw createError( + initializer, "The left-hand side of a 'for...in' statement cannot be a 'using' declaration.", - initializer.getSourceFile(), - initializer.getStart(), - initializer.getEnd(), ); } } else if ( @@ -68,10 +60,8 @@ function checkForStatementDeclaration( initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression ) { throw createError( + initializer, `The left-hand side of a '${loop}' statement must be a variable or a property access.`, - initializer.getSourceFile(), - initializer.getStart(), - initializer.getEnd(), ); } } From ee1ada9db5229902f70faa19dc27950dc457cc7d Mon Sep 17 00:00:00 2001 From: Lonercode Date: Thu, 27 Nov 2025 16:39:34 +0100 Subject: [PATCH 14/15] chore: minor update --- packages/typescript-estree/src/check-syntax-errors.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/typescript-estree/src/check-syntax-errors.ts b/packages/typescript-estree/src/check-syntax-errors.ts index 048b18f80d4a..b67f3cf303b2 100644 --- a/packages/typescript-estree/src/check-syntax-errors.ts +++ b/packages/typescript-estree/src/check-syntax-errors.ts @@ -10,8 +10,7 @@ export function checkSyntaxError(node: ts.Node): void { case ts.SyntaxKind.ForInStatement: case ts.SyntaxKind.ForOfStatement: { checkForStatementDeclaration( - (node as ts.ForInStatement | ts.ForOfStatement).initializer, - node.kind, + node as ts.ForInStatement | ts.ForOfStatement, ); break; } @@ -22,9 +21,9 @@ export function checkSyntaxError(node: ts.Node): void { } function checkForStatementDeclaration( - initializer: ts.ForInitializer, - kind: ts.SyntaxKind, + node: ts.ForInStatement | ts.ForOfStatement, ): void { + const { initializer, kind } = node; const loop = kind === ts.SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; if (ts.isVariableDeclarationList(initializer)) { if (initializer.declarations.length !== 1) { From 29a9c737f1689403891c015c60223efa998fdf2f Mon Sep 17 00:00:00 2001 From: Lonercode Date: Thu, 27 Nov 2025 18:22:54 +0100 Subject: [PATCH 15/15] chore: update based on reviews --- .../src/check-syntax-errors.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/typescript-estree/src/check-syntax-errors.ts b/packages/typescript-estree/src/check-syntax-errors.ts index b67f3cf303b2..1eb780abcc7d 100644 --- a/packages/typescript-estree/src/check-syntax-errors.ts +++ b/packages/typescript-estree/src/check-syntax-errors.ts @@ -1,17 +1,21 @@ import * as ts from 'typescript'; +import type { TSNode } from './ts-estree'; + import { checkModifiers } from './check-modifiers'; import { isValidAssignmentTarget, createError } from './node-utils'; -export function checkSyntaxError(node: ts.Node): void { - checkModifiers(node); +const SyntaxKind = ts.SyntaxKind; + +export function checkSyntaxError(tsNode: ts.Node): void { + checkModifiers(tsNode); + + const node = tsNode as TSNode; switch (node.kind) { - case ts.SyntaxKind.ForInStatement: - case ts.SyntaxKind.ForOfStatement: { - checkForStatementDeclaration( - node as ts.ForInStatement | ts.ForOfStatement, - ); + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: { + checkForStatementDeclaration(node); break; } default: { @@ -24,7 +28,7 @@ function checkForStatementDeclaration( node: ts.ForInStatement | ts.ForOfStatement, ): void { const { initializer, kind } = node; - const loop = kind === ts.SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; + const loop = kind === SyntaxKind.ForInStatement ? 'for...in' : 'for...of'; if (ts.isVariableDeclarationList(initializer)) { if (initializer.declarations.length !== 1) { throw createError( @@ -45,7 +49,7 @@ function checkForStatementDeclaration( ); } if ( - kind === ts.SyntaxKind.ForInStatement && + kind === SyntaxKind.ForInStatement && initializer.flags & ts.NodeFlags.Using ) { throw createError( @@ -55,8 +59,8 @@ function checkForStatementDeclaration( } } else if ( !isValidAssignmentTarget(initializer) && - initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression && - initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression + initializer.kind !== SyntaxKind.ObjectLiteralExpression && + initializer.kind !== SyntaxKind.ArrayLiteralExpression ) { throw createError( initializer,