From 5c182e669e1bfecb52b09e5a273fc1df5569c7b0 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 5 Jun 2025 14:22:43 +0800 Subject: [PATCH 1/4] fix(typescript-estree): add validation to interface extends --- .../snapshots/1-TSESTree-Error.shot | 5 ++- .../snapshots/3-Alignment-Error.shot | 2 +- .../snapshots/1-TSESTree-Error.shot | 7 +++- .../snapshots/3-Alignment-Error.shot | 2 +- .../fixtures-with-differences-errors.shot | 2 -- packages/typescript-estree/src/convert.ts | 33 +++++++++++++++++++ 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/1-TSESTree-Error.shot b/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/1-TSESTree-Error.shot index 3192c89d0208..70781ef9b372 100644 --- a/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/1-TSESTree-Error.shot +++ b/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/1-TSESTree-Error.shot @@ -1,4 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`AST Fixtures > declaration > TSInterfaceDeclaration > _error_ > non-identifier-extends > TSESTree - Error`] -NO ERROR +TSError +> 1 | interface F extends 1 {} + | ^ Interface declaration can only extend an identifier/qualified name with optional type arguments. + 2 | diff --git a/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/3-Alignment-Error.shot b/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/3-Alignment-Error.shot index 8b8a05459c4b..a0844838800f 100644 --- a/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/3-Alignment-Error.shot +++ b/packages/ast-spec/src/declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/snapshots/3-Alignment-Error.shot @@ -1,4 +1,4 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`AST Fixtures > declaration > TSInterfaceDeclaration > _error_ > non-identifier-extends > Error Alignment`] -Babel errored but TSESTree didn't +Both errored diff --git a/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/1-TSESTree-Error.shot b/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/1-TSESTree-Error.shot index b9716b289515..f57a1b692cd1 100644 --- a/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/1-TSESTree-Error.shot +++ b/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/1-TSESTree-Error.shot @@ -1,4 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`AST Fixtures > legacy-fixtures > errorRecovery > _error_ > interface-multiple-extends > TSESTree - Error`] -NO ERROR +TSError + 1 | // TODO: This fixture might be too large, and if so should be split up. + 2 | +> 3 | interface foo extends bar extends baz {} + | ^^^^^^^^^^^ 'extends' clause already seen. + 4 | diff --git a/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/3-Alignment-Error.shot b/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/3-Alignment-Error.shot index 8c05c9ec9327..2b5ef310d7cd 100644 --- a/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/3-Alignment-Error.shot +++ b/packages/ast-spec/src/legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/snapshots/3-Alignment-Error.shot @@ -1,4 +1,4 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`AST Fixtures > legacy-fixtures > errorRecovery > _error_ > interface-multiple-extends > Error Alignment`] -Babel errored but TSESTree didn't +Both errored diff --git a/packages/ast-spec/tests/fixtures-with-differences-errors.shot b/packages/ast-spec/tests/fixtures-with-differences-errors.shot index ef7f47117298..7051aa777fbb 100644 --- a/packages/ast-spec/tests/fixtures-with-differences-errors.shot +++ b/packages/ast-spec/tests/fixtures-with-differences-errors.shot @@ -12,7 +12,6 @@ exports[`AST Fixtures > List fixtures with Error differences`] "declaration/TSImportEqualsDeclaration/fixtures/_error_/import-kind/fixture.ts", "declaration/TSInterfaceDeclaration/fixtures/_error_/missing-extends/fixture.ts", "declaration/TSInterfaceDeclaration/fixtures/_error_/missing-type-param/fixture.ts", - "declaration/TSInterfaceDeclaration/fixtures/_error_/non-identifier-extends/fixture.ts", "declaration/TSTypeAliasDeclaration/fixtures/_error_/missing-type-parameter/fixture.ts", "declaration/VariableDeclaration/fixtures/_error_/const-destructure-no-init/fixture.ts", "declaration/VariableDeclaration/fixtures/_error_/const-destructure-type-no-init/fixture.ts", @@ -45,7 +44,6 @@ exports[`AST Fixtures > List fixtures with Error differences`] "legacy-fixtures/errorRecovery/fixtures/_error_/empty-type-parameters/fixture.ts", "legacy-fixtures/errorRecovery/fixtures/_error_/index-signature-parameters/fixture.ts", "legacy-fixtures/errorRecovery/fixtures/_error_/interface-empty-extends/fixture.ts", - "legacy-fixtures/errorRecovery/fixtures/_error_/interface-multiple-extends/fixture.ts", "legacy-fixtures/errorRecovery/fixtures/_error_/interface-with-optional-index-signature/fixture.ts", "legacy-fixtures/parameter-decorators/fixtures/_error_/parameter-array-pattern-decorator/fixture.ts", "legacy-fixtures/parameter-decorators/fixtures/_error_/parameter-rest-element-decorator/fixture.ts", diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 22c0257505a2..8f293db2af02 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -70,6 +70,25 @@ export interface ASTMaps { tsNodeToESTreeNodeMap: ParserWeakMap; } +function isPropertyAccessEntityNameExpression( + node: ts.Node, +): node is ts.PropertyAccessEntityNameExpression { + return ( + ts.isPropertyAccessExpression(node) && + ts.isIdentifier(node.name) && + isEntityNameExpression(node.expression) + ); +} + +function isEntityNameExpression( + node: ts.Node, +): node is ts.EntityNameExpression { + return ( + node.kind === SyntaxKind.Identifier || + isPropertyAccessEntityNameExpression(node) + ); +} + export class Converter { private allowPattern = false; private readonly ast: ts.SourceFile; @@ -3024,6 +3043,7 @@ export class Converter { const interfaceHeritageClauses = node.heritageClauses ?? []; const interfaceExtends: TSESTree.TSInterfaceHeritage[] = []; + let seenExtendsClause = false; for (const heritageClause of interfaceHeritageClauses) { if (heritageClause.token !== SyntaxKind.ExtendsKeyword) { this.#throwError( @@ -3033,8 +3053,21 @@ export class Converter { : 'Unexpected token.', ); } + if (seenExtendsClause) { + this.#throwError(heritageClause, "'extends' clause already seen."); + } + seenExtendsClause = true; for (const heritageType of heritageClause.types) { + if ( + !isEntityNameExpression(heritageType.expression) || + ts.isOptionalChain(heritageType.expression) + ) { + this.#throwError( + heritageType, + 'Interface declaration can only extend an identifier/qualified name with optional type arguments.', + ); + } interfaceExtends.push( this.convertChild( heritageType, From 7e5ac008127b567748f230a6e7de0fb10fcbfb78 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 5 Jun 2025 17:07:02 +0800 Subject: [PATCH 2/4] remove an outdated test case --- .../no-unused-vars/no-unused-vars.test.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 5ebc8d9a406b..5f40bbcdc3cc 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -360,25 +360,6 @@ export interface Bar extends baz.test {} code: ` import test from 'test'; import baz from 'baz'; -export interface Bar extends baz().test {} - `, - errors: [ - { - column: 8, - data: { - action: 'defined', - additional: '', - varName: 'test', - }, - line: 2, - messageId: 'unusedVar', - }, - ], - }, - { - code: ` -import test from 'test'; -import baz from 'baz'; export class Bar implements baz.test {} `, errors: [ From 2332e9680fdcc89b544e2f39c0605f57d3c2717a Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 5 Jun 2025 20:03:55 +0800 Subject: [PATCH 3/4] increase the patch coverage --- .../tests/lib/convert.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/typescript-estree/tests/lib/convert.test.ts b/packages/typescript-estree/tests/lib/convert.test.ts index 77dea1f5f4b4..97ba346c5e79 100644 --- a/packages/typescript-estree/tests/lib/convert.test.ts +++ b/packages/typescript-estree/tests/lib/convert.test.ts @@ -449,4 +449,25 @@ describe('convert', () => { expect(Object.keys(tsMappedType)).toContain('typeParameter'); }); }); + + describe('should throw error for invalid interface extends clauses', () => { + it('should throw error for multiple extends', () => { + const code = 'interface Foo extends Bar extends Bar {}'; + + const instance = new Converter(convertCode(code)); + + expect(() => instance.convertProgram()).toThrow( + "'extends' clause already seen.", + ); + }); + it('should throw error for optional chain', () => { + const code = 'interface Foo extends Bar?.Bar {}'; + + const instance = new Converter(convertCode(code)); + + expect(() => instance.convertProgram()).toThrow( + 'Interface declaration can only extend an identifier/qualified name with optional type arguments.', + ); + }); + }); }); From 50fea5e80966b2b84ecd3588639100465f17c0ec Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 9 Jun 2025 16:43:45 +0930 Subject: [PATCH 4/4] Discard changes to packages/typescript-estree/tests/lib/convert.test.ts --- .../tests/lib/convert.test.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/typescript-estree/tests/lib/convert.test.ts b/packages/typescript-estree/tests/lib/convert.test.ts index 97ba346c5e79..77dea1f5f4b4 100644 --- a/packages/typescript-estree/tests/lib/convert.test.ts +++ b/packages/typescript-estree/tests/lib/convert.test.ts @@ -449,25 +449,4 @@ describe('convert', () => { expect(Object.keys(tsMappedType)).toContain('typeParameter'); }); }); - - describe('should throw error for invalid interface extends clauses', () => { - it('should throw error for multiple extends', () => { - const code = 'interface Foo extends Bar extends Bar {}'; - - const instance = new Converter(convertCode(code)); - - expect(() => instance.convertProgram()).toThrow( - "'extends' clause already seen.", - ); - }); - it('should throw error for optional chain', () => { - const code = 'interface Foo extends Bar?.Bar {}'; - - const instance = new Converter(convertCode(code)); - - expect(() => instance.convertProgram()).toThrow( - 'Interface declaration can only extend an identifier/qualified name with optional type arguments.', - ); - }); - }); });