From 459b499786e3c1d8200010fd7318799d33e4a91d Mon Sep 17 00:00:00 2001 From: Oleksandr Tarasiuk Date: Fri, 28 Aug 2026 10:53:55 +0300 Subject: [PATCH 1/3] fix(64058): fix reparse jsdoc @extends type arguments for call expressions --- tsc/internal/ast/utilities.go | 24 ++++++-- tsc/internal/parser/parser_test.go | 60 +++++++++++++++++++ .../reference/conformance/extendsTag7.symbols | 37 ++++++++++++ .../reference/conformance/extendsTag7.types | 40 +++++++++++++ .../conformance/extendsTag8.errors.txt | 20 +++++++ .../reference/conformance/extendsTag8.symbols | 27 +++++++++ .../reference/conformance/extendsTag8.types | 28 +++++++++ .../cases/conformance/jsdoc/extendsTag7.ts | 23 +++++++ .../cases/conformance/jsdoc/extendsTag8.ts | 19 ++++++ 9 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag7.symbols create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag7.types create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag8.symbols create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag8.types create mode 100644 tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts create mode 100644 tsc/testdata/tests/cases/conformance/jsdoc/extendsTag8.ts diff --git a/tsc/internal/ast/utilities.go b/tsc/internal/ast/utilities.go index 42058ea2c399f..fac1bdec9375a 100644 --- a/tsc/internal/ast/utilities.go +++ b/tsc/internal/ast/utilities.go @@ -1645,12 +1645,24 @@ func IsDottedName(node *Node) bool { return false } -func HasSamePropertyAccessName(node1, node2 *Node) bool { - if node1.Kind == KindIdentifier && node2.Kind == KindIdentifier { - return node1.Text() == node2.Text() - } else if node1.Kind == KindPropertyAccessExpression && node2.Kind == KindPropertyAccessExpression { - return node1.AsPropertyAccessExpression().Name().Text() == node2.AsPropertyAccessExpression().Name().Text() && - HasSamePropertyAccessName(node1.Expression(), node2.Expression()) +func HasSamePropertyAccessName(target, source *Node) bool { + target = SkipParentheses(target) + source = SkipParentheses(source) + if IsCallExpression(target) { + target = SkipParentheses(target.Expression()) + if IsPropertyAccessExpression(target) { + if HasSamePropertyAccessName(target, source) { + return true + } + target = SkipParentheses(target.Expression()) + } + } + if target.Kind == KindIdentifier && source.Kind == KindIdentifier { + return target.Text() == source.Text() + } + if target.Kind == KindPropertyAccessExpression && source.Kind == KindPropertyAccessExpression { + return target.AsPropertyAccessExpression().Name().Text() == source.AsPropertyAccessExpression().Name().Text() && + HasSamePropertyAccessName(target.Expression(), source.Expression()) } return false } diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index 200064160228b..50f5a86608609 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -308,6 +308,66 @@ function foo(options) {}` assert.Equal(t, scanner.GetTokenPosOfNode(typeNode, file, false /*includeJSDoc*/), strings.Index(sourceText, "{{")+1) } +func TestJSDocAugmentsTypeArgumentsReparse(t *testing.T) { + t.Parallel() + sourceText := `/** @template T */ +class A { + static extend() { + return this; + } +} + +/** @extends {A} */ +class B extends A.extend() {} + +/** @extends {Other} */ +class C extends A.extend() {}` + opts := ast.SourceFileParseOptions{ + FileName: "/index.js", + Path: "/index.js", + } + + file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS) + statements := file.Statements.Nodes + assert.Equal(t, len(statements), 3) + + classB := statements[1] + assert.Assert(t, ast.IsClassDeclaration(classB)) + + baseType := ast.GetClassExtendsHeritageElement(classB) + assert.Assert(t, baseType != nil) + assert.Assert(t, ast.IsCallExpression(baseType.Expression())) + + typeArguments := baseType.TypeArguments() + assert.Equal(t, len(typeArguments), 1) + + typeArgument := typeArguments[0] + assert.Equal(t, typeArgument.Kind, ast.KindStringKeyword) + assert.Assert(t, typeArgument.Flags&ast.NodeFlagsReparsed != 0) + + jsDocs := classB.JSDoc(file) + assert.Equal(t, len(jsDocs), 1) + + tags := jsDocs[0].AsJSDoc().Tags + assert.Assert(t, tags != nil) + assert.Equal(t, len(tags.Nodes), 1) + + tag := tags.Nodes[0] + assert.Assert(t, ast.IsJSDocAugmentsTag(tag)) + + sourceTypeArguments := tag.ClassName().TypeArguments() + assert.Equal(t, len(sourceTypeArguments), 1) + assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), typeArgument) + + classC := statements[2] + assert.Assert(t, ast.IsClassDeclaration(classC)) + + mismatchedBaseType := ast.GetClassExtendsHeritageElement(classC) + assert.Assert(t, mismatchedBaseType != nil) + assert.Assert(t, ast.IsCallExpression(mismatchedBaseType.Expression())) + assert.Equal(t, len(mismatchedBaseType.TypeArguments()), 0) +} + func TestSourceFilePositionMapWithNonASCIIStringLiteral(t *testing.T) { t.Parallel() sourceText := `const x = "─"; diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag7.symbols b/tsc/testdata/baselines/reference/conformance/extendsTag7.symbols new file mode 100644 index 0000000000000..20349b715c45d --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag7.symbols @@ -0,0 +1,37 @@ +//// [tests/cases/conformance/jsdoc/extendsTag7.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : Symbol(A, Decl(a.js, 0, 0)) + + /** @returns {T} */ + get value() { +>value : Symbol(A.value, Decl(a.js, 1, 9)) + + throw new Error(); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + + /** @returns {typeof A} */ + static extend() { +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + + return this; +>this : Symbol(A, Decl(a.js, 0, 0)) + } +} + +/** @extends {A} */ +class B extends A.extend() {} +>B : Symbol(B, Decl(a.js, 11, 1)) +>A.extend : Symbol(A.extend, Decl(a.js, 5, 5)) +>A : Symbol(A, Decl(a.js, 0, 0)) +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + +const value = new B().value; +>value : Symbol(value, Decl(a.js, 16, 5)) +>new B().value : Symbol(A.value, Decl(a.js, 1, 9)) +>B : Symbol(B, Decl(a.js, 11, 1)) +>value : Symbol(A.value, Decl(a.js, 1, 9)) + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag7.types b/tsc/testdata/baselines/reference/conformance/extendsTag7.types new file mode 100644 index 0000000000000..50b3b50060c01 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag7.types @@ -0,0 +1,40 @@ +//// [tests/cases/conformance/jsdoc/extendsTag7.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : A + + /** @returns {T} */ + get value() { +>value : T + + throw new Error(); +>new Error() : Error +>Error : ErrorConstructor + } + + /** @returns {typeof A} */ + static extend() { +>extend : () => typeof A + + return this; +>this : typeof A + } +} + +/** @extends {A} */ +class B extends A.extend() {} +>B : B +>A.extend() : A +>A.extend : () => typeof A +>A : typeof A +>extend : () => typeof A + +const value = new B().value; +>value : string +>new B().value : string +>new B() : B +>B : typeof B +>value : string + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt b/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt new file mode 100644 index 0000000000000..1d1b5a53efbe2 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt @@ -0,0 +1,20 @@ +a.js(13,17): error TS8026: Expected A type arguments; provide these with an '@extends' tag. + + +==== a.js (1 errors) ==== + /** @template T */ + class A { + /** @returns {typeof A} */ + static extend() { + return this; + } + } + + /** @template T */ + class C {} + + /** @extends {C} */ + class B extends A.extend() {} + ~~~~~~~~~~ +!!! error TS8026: Expected A type arguments; provide these with an '@extends' tag. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.symbols b/tsc/testdata/baselines/reference/conformance/extendsTag8.symbols new file mode 100644 index 0000000000000..8b10f3499d474 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.symbols @@ -0,0 +1,27 @@ +//// [tests/cases/conformance/jsdoc/extendsTag8.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : Symbol(A, Decl(a.js, 0, 0)) + + /** @returns {typeof A} */ + static extend() { +>extend : Symbol(A.extend, Decl(a.js, 1, 9)) + + return this; +>this : Symbol(A, Decl(a.js, 0, 0)) + } +} + +/** @template T */ +class C {} +>C : Symbol(C, Decl(a.js, 6, 1)) + +/** @extends {C} */ +class B extends A.extend() {} +>B : Symbol(B, Decl(a.js, 9, 10)) +>A.extend : Symbol(A.extend, Decl(a.js, 1, 9)) +>A : Symbol(A, Decl(a.js, 0, 0)) +>extend : Symbol(A.extend, Decl(a.js, 1, 9)) + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.types b/tsc/testdata/baselines/reference/conformance/extendsTag8.types new file mode 100644 index 0000000000000..9c62961a7581e --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.types @@ -0,0 +1,28 @@ +//// [tests/cases/conformance/jsdoc/extendsTag8.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : A + + /** @returns {typeof A} */ + static extend() { +>extend : () => typeof A + + return this; +>this : typeof A + } +} + +/** @template T */ +class C {} +>C : C + +/** @extends {C} */ +class B extends A.extend() {} +>B : B +>A.extend() : A +>A.extend : () => typeof A +>A : typeof A +>extend : () => typeof A + diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts new file mode 100644 index 0000000000000..6177d00b31490 --- /dev/null +++ b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts @@ -0,0 +1,23 @@ +// @target: es2015 +// @allowJs: true +// @checkJs: true +// @noEmit: true + +// @filename: a.js +/** @template T */ +class A { + /** @returns {T} */ + get value() { + throw new Error(); + } + + /** @returns {typeof A} */ + static extend() { + return this; + } +} + +/** @extends {A} */ +class B extends A.extend() {} + +const value = new B().value; diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag8.ts b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag8.ts new file mode 100644 index 0000000000000..d63c39d8f257c --- /dev/null +++ b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag8.ts @@ -0,0 +1,19 @@ +// @target: es2015 +// @allowJs: true +// @checkJs: true +// @noEmit: true + +// @filename: a.js +/** @template T */ +class A { + /** @returns {typeof A} */ + static extend() { + return this; + } +} + +/** @template T */ +class C {} + +/** @extends {C} */ +class B extends A.extend() {} From a315a461b846553c5586a6b5d30f9b3e2fa90ba6 Mon Sep 17 00:00:00 2001 From: Oleksandr Tarasiuk Date: Fri, 28 Aug 2026 23:39:37 +0300 Subject: [PATCH 2/3] preserve jsdoc base identity for call-expression heritage --- tsc/internal/ast/utilities.go | 24 ++------ tsc/internal/checker/checker.go | 13 +++- tsc/internal/parser/parser_test.go | 18 ++---- tsc/internal/parser/reparser.go | 20 +++--- .../reference/conformance/extendsTag7.js | 54 ++++++++++++++++ .../conformance/extendsTag8.errors.txt | 6 +- .../reference/conformance/extendsTag8.types | 2 +- .../conformance/extendsTag9.errors.txt | 36 +++++++++++ .../reference/conformance/extendsTag9.symbols | 54 ++++++++++++++++ .../reference/conformance/extendsTag9.types | 61 +++++++++++++++++++ .../cases/conformance/jsdoc/extendsTag7.ts | 3 +- .../cases/conformance/jsdoc/extendsTag9.ts | 32 ++++++++++ 12 files changed, 276 insertions(+), 47 deletions(-) create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag7.js create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag9.symbols create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag9.types create mode 100644 tsc/testdata/tests/cases/conformance/jsdoc/extendsTag9.ts diff --git a/tsc/internal/ast/utilities.go b/tsc/internal/ast/utilities.go index fac1bdec9375a..42058ea2c399f 100644 --- a/tsc/internal/ast/utilities.go +++ b/tsc/internal/ast/utilities.go @@ -1645,24 +1645,12 @@ func IsDottedName(node *Node) bool { return false } -func HasSamePropertyAccessName(target, source *Node) bool { - target = SkipParentheses(target) - source = SkipParentheses(source) - if IsCallExpression(target) { - target = SkipParentheses(target.Expression()) - if IsPropertyAccessExpression(target) { - if HasSamePropertyAccessName(target, source) { - return true - } - target = SkipParentheses(target.Expression()) - } - } - if target.Kind == KindIdentifier && source.Kind == KindIdentifier { - return target.Text() == source.Text() - } - if target.Kind == KindPropertyAccessExpression && source.Kind == KindPropertyAccessExpression { - return target.AsPropertyAccessExpression().Name().Text() == source.AsPropertyAccessExpression().Name().Text() && - HasSamePropertyAccessName(target.Expression(), source.Expression()) +func HasSamePropertyAccessName(node1, node2 *Node) bool { + if node1.Kind == KindIdentifier && node2.Kind == KindIdentifier { + return node1.Text() == node2.Text() + } else if node1.Kind == KindPropertyAccessExpression && node2.Kind == KindPropertyAccessExpression { + return node1.AsPropertyAccessExpression().Name().Text() == node2.AsPropertyAccessExpression().Name().Text() && + HasSamePropertyAccessName(node1.Expression(), node2.Expression()) } return false } diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index fb3c33c01b814..0a3ae8a65dd41 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -4417,11 +4417,20 @@ func (c *Checker) checkJSDocAugmentsTagMatchesExtends(node *ast.Node, baseTypeNo continue } sourceTypeNode := tag.ClassName() - if c.isTypeIdenticalTo(c.getTypeFromTypeNode(sourceTypeNode), baseType) { + sourceType := c.getTypeFromTypeNode(sourceTypeNode) + sourceName := getIdentifierFromEntityNameExpression(sourceTypeNode.Expression()) + if sourceName != nil && ast.IsCallExpression(ast.SkipParentheses(baseTypeNode.Expression())) { + sourceSymbol := c.getMergedSymbol(getTargetType(sourceType).symbol) + targetSymbol := c.getMergedSymbol(getTargetType(baseType).symbol) + if sourceSymbol != nil && targetSymbol != nil && sourceSymbol != targetSymbol { + c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), targetSymbol.Name) + continue + } + } + if c.isTypeIdenticalTo(sourceType, baseType) { continue } targetName := getIdentifierFromEntityNameExpression(baseTypeNode.Expression()) - sourceName := getIdentifierFromEntityNameExpression(sourceTypeNode.Expression()) if targetName != nil && sourceName != nil { c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), targetName.Text()) } diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index 50f5a86608609..e168543c602c6 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -308,7 +308,7 @@ function foo(options) {}` assert.Equal(t, scanner.GetTokenPosOfNode(typeNode, file, false /*includeJSDoc*/), strings.Index(sourceText, "{{")+1) } -func TestJSDocAugmentsTypeArgumentsReparse(t *testing.T) { +func TestJSDocAugmentsCallHeritage(t *testing.T) { t.Parallel() sourceText := `/** @template T */ class A { @@ -318,10 +318,7 @@ class A { } /** @extends {A} */ -class B extends A.extend() {} - -/** @extends {Other} */ -class C extends A.extend() {}` +class B extends A.extend() {}` opts := ast.SourceFileParseOptions{ FileName: "/index.js", Path: "/index.js", @@ -329,7 +326,7 @@ class C extends A.extend() {}` file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS) statements := file.Statements.Nodes - assert.Equal(t, len(statements), 3) + assert.Equal(t, len(statements), 2) classB := statements[1] assert.Assert(t, ast.IsClassDeclaration(classB)) @@ -337,6 +334,7 @@ class C extends A.extend() {}` baseType := ast.GetClassExtendsHeritageElement(classB) assert.Assert(t, baseType != nil) assert.Assert(t, ast.IsCallExpression(baseType.Expression())) + assert.Equal(t, scanner.GetTextOfNode(baseType.Expression()), "A.extend()") typeArguments := baseType.TypeArguments() assert.Equal(t, len(typeArguments), 1) @@ -358,14 +356,6 @@ class C extends A.extend() {}` sourceTypeArguments := tag.ClassName().TypeArguments() assert.Equal(t, len(sourceTypeArguments), 1) assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), typeArgument) - - classC := statements[2] - assert.Assert(t, ast.IsClassDeclaration(classC)) - - mismatchedBaseType := ast.GetClassExtendsHeritageElement(classC) - assert.Assert(t, mismatchedBaseType != nil) - assert.Assert(t, ast.IsCallExpression(mismatchedBaseType.Expression())) - assert.Equal(t, len(mismatchedBaseType.TypeArguments()), 0) } func TestSourceFilePositionMapWithNonASCIIStringLiteral(t *testing.T) { diff --git a/tsc/internal/parser/reparser.go b/tsc/internal/parser/reparser.go index 58a58cf974bd6..947afb69e613a 100644 --- a/tsc/internal/parser/reparser.go +++ b/tsc/internal/parser/reparser.go @@ -597,14 +597,9 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node) }); extendsClause != nil && len(extendsClause.AsHeritageClause().Types.Nodes) == 1 { target := extendsClause.AsHeritageClause().Types.Nodes[0].AsExpressionWithTypeArguments() source := tag.ClassName().AsExpressionWithTypeArguments() - if ast.HasSamePropertyAccessName(target.Expression, source.Expression) { - if target.TypeArguments == nil && source.TypeArguments != nil { - newArguments := p.nodeSliceArena.NewSlice(len(source.TypeArguments.Nodes)) - for i, arg := range source.TypeArguments.Nodes { - newArguments[i] = p.addDeepCloneReparse(arg) - } - target.TypeArguments = p.newNodeList(source.TypeArguments.Loc, newArguments) - p.finishMutatedNode(target.AsNode()) + if target.TypeArguments == nil && source.TypeArguments != nil { + if ast.IsCallExpression(ast.SkipParentheses(target.Expression)) || ast.HasSamePropertyAccessName(target.Expression, source.Expression) { + p.setReparsedTypeArguments(target, source.TypeArguments) } } } @@ -612,6 +607,15 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node) } } +func (p *Parser) setReparsedTypeArguments(target *ast.ExpressionWithTypeArguments, source *ast.NodeList) { + typeArguments := p.nodeSliceArena.NewSlice(len(source.Nodes)) + for i, typeArgument := range source.Nodes { + typeArguments[i] = p.addDeepCloneReparse(typeArgument) + } + target.TypeArguments = p.newNodeList(source.Loc, typeArguments) + p.finishMutatedNode(target.AsNode()) +} + func (p *Parser) makeQuestionIfOptional(parameter *ast.JSDocParameterOrPropertyTag) *ast.Node { var questionToken *ast.Node if parameter.IsBracketed || parameter.TypeExpression != nil && parameter.TypeExpression.Type().Kind == ast.KindJSDocOptionalType { diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag7.js b/tsc/testdata/baselines/reference/conformance/extendsTag7.js new file mode 100644 index 0000000000000..03a22db1d8266 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag7.js @@ -0,0 +1,54 @@ +//// [tests/cases/conformance/jsdoc/extendsTag7.ts] //// + +//// [a.js] +/** @template T */ +class A { + /** @returns {T} */ + get value() { + throw new Error(); + } + + /** @returns {typeof A} */ + static extend() { + return this; + } +} + +/** @extends {A} */ +class B extends A.extend() {} + +const value = new B().value; + + +//// [a.js] +"use strict"; +/** @template T */ +class A { + /** @returns {T} */ + get value() { + throw new Error(); + } + /** @returns {typeof A} */ + static extend() { + return this; + } +} +/** @extends {A} */ +class B extends A.extend() { +} +const value = new B().value; + + +//// [a.d.ts] +/** @template T */ +declare class A { + /** @returns {T} */ + get value(): T; + /** @returns {typeof A} */ + static extend(): typeof A; +} +declare const B_base: typeof A; +/** @extends {A} */ +declare class B extends B_base { +} +declare const value: string; diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt b/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt index 1d1b5a53efbe2..a831aebd4bbd7 100644 --- a/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt @@ -1,4 +1,4 @@ -a.js(13,17): error TS8026: Expected A type arguments; provide these with an '@extends' tag. +a.js(12,15): error TS8023: JSDoc '@extends C' does not match the 'extends A' clause. ==== a.js (1 errors) ==== @@ -14,7 +14,7 @@ a.js(13,17): error TS8026: Expected A type arguments; provide these with an ' class C {} /** @extends {C} */ + ~ +!!! error TS8023: JSDoc '@extends C' does not match the 'extends A' clause. class B extends A.extend() {} - ~~~~~~~~~~ -!!! error TS8026: Expected A type arguments; provide these with an '@extends' tag. \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.types b/tsc/testdata/baselines/reference/conformance/extendsTag8.types index 9c62961a7581e..4c71958d321c1 100644 --- a/tsc/testdata/baselines/reference/conformance/extendsTag8.types +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.types @@ -21,7 +21,7 @@ class C {} /** @extends {C} */ class B extends A.extend() {} >B : B ->A.extend() : A +>A.extend() : A >A.extend : () => typeof A >A : typeof A >extend : () => typeof A diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt b/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt new file mode 100644 index 0000000000000..218b8ea0a3000 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt @@ -0,0 +1,36 @@ +a.js(22,15): error TS8023: JSDoc '@extends A' does not match the 'extends C' clause. +a.js(25,19): error TS2339: Property 'a' does not exist on type 'B'. + + +==== a.js (2 errors) ==== + /** @template T */ + class A { + /** @returns {T} */ + get a() { + throw new Error(); + } + + /** @returns {typeof C} */ + static extend() { + return C; + } + } + + /** @template T */ + class C { + /** @returns {T} */ + get c() { + throw new Error(); + } + } + + /** @extends {A} */ + ~ +!!! error TS8023: JSDoc '@extends A' does not match the 'extends C' clause. + class B extends A.extend() {} + + const a = new B().a; + ~ +!!! error TS2339: Property 'a' does not exist on type 'B'. + const c = new B().c; + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag9.symbols b/tsc/testdata/baselines/reference/conformance/extendsTag9.symbols new file mode 100644 index 0000000000000..6ff2bd61bdd27 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag9.symbols @@ -0,0 +1,54 @@ +//// [tests/cases/conformance/jsdoc/extendsTag9.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : Symbol(A, Decl(a.js, 0, 0)) + + /** @returns {T} */ + get a() { +>a : Symbol(A.a, Decl(a.js, 1, 9)) + + throw new Error(); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + + /** @returns {typeof C} */ + static extend() { +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + + return C; +>C : Symbol(C, Decl(a.js, 11, 1)) + } +} + +/** @template T */ +class C { +>C : Symbol(C, Decl(a.js, 11, 1)) + + /** @returns {T} */ + get c() { +>c : Symbol(C.c, Decl(a.js, 14, 9)) + + throw new Error(); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } +} + +/** @extends {A} */ +class B extends A.extend() {} +>B : Symbol(B, Decl(a.js, 19, 1)) +>A.extend : Symbol(A.extend, Decl(a.js, 5, 5)) +>A : Symbol(A, Decl(a.js, 0, 0)) +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + +const a = new B().a; +>a : Symbol(a, Decl(a.js, 24, 5)) +>B : Symbol(B, Decl(a.js, 19, 1)) + +const c = new B().c; +>c : Symbol(c, Decl(a.js, 25, 5)) +>new B().c : Symbol(C.c, Decl(a.js, 14, 9)) +>B : Symbol(B, Decl(a.js, 19, 1)) +>c : Symbol(C.c, Decl(a.js, 14, 9)) + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag9.types b/tsc/testdata/baselines/reference/conformance/extendsTag9.types new file mode 100644 index 0000000000000..42d02b953dc31 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag9.types @@ -0,0 +1,61 @@ +//// [tests/cases/conformance/jsdoc/extendsTag9.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : A + + /** @returns {T} */ + get a() { +>a : T + + throw new Error(); +>new Error() : Error +>Error : ErrorConstructor + } + + /** @returns {typeof C} */ + static extend() { +>extend : () => typeof C + + return C; +>C : typeof C + } +} + +/** @template T */ +class C { +>C : C + + /** @returns {T} */ + get c() { +>c : T + + throw new Error(); +>new Error() : Error +>Error : ErrorConstructor + } +} + +/** @extends {A} */ +class B extends A.extend() {} +>B : B +>A.extend() : C +>A.extend : () => typeof C +>A : typeof A +>extend : () => typeof C + +const a = new B().a; +>a : any +>new B().a : any +>new B() : B +>B : typeof B +>a : any + +const c = new B().c; +>c : string +>new B().c : string +>new B() : B +>B : typeof B +>c : string + diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts index 6177d00b31490..3733efd21dbce 100644 --- a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts +++ b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts @@ -1,7 +1,8 @@ // @target: es2015 // @allowJs: true // @checkJs: true -// @noEmit: true +// @declaration: true +// @outDir: out // @filename: a.js /** @template T */ diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag9.ts b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag9.ts new file mode 100644 index 0000000000000..a5fe80b7e5fde --- /dev/null +++ b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag9.ts @@ -0,0 +1,32 @@ +// @target: es2015 +// @allowJs: true +// @checkJs: true +// @noEmit: true + +// @filename: a.js +/** @template T */ +class A { + /** @returns {T} */ + get a() { + throw new Error(); + } + + /** @returns {typeof C} */ + static extend() { + return C; + } +} + +/** @template T */ +class C { + /** @returns {T} */ + get c() { + throw new Error(); + } +} + +/** @extends {A} */ +class B extends A.extend() {} + +const a = new B().a; +const c = new B().c; From 929b1bc42010c04de0cde0d430c2a3a314d7b0ee Mon Sep 17 00:00:00 2001 From: Oleksandr Tarasiuk Date: Thu, 3 Sep 2026 12:44:16 +0300 Subject: [PATCH 3/3] resolve jsdoc @extends calls before applying type arguments --- tsc/internal/ast/utilities.go | 18 +++++ tsc/internal/checker/checker.go | 47 ++--------- tsc/internal/checker/emitresolver.go | 11 +++ tsc/internal/checker/jsdoc.go | 62 ++++++++++++++ tsc/internal/checker/utilities.go | 15 ++++ tsc/internal/parser/parser_test.go | 11 +-- tsc/internal/parser/reparser.go | 6 +- tsc/internal/printer/emitresolver.go | 1 + .../transformers/declarations/transform.go | 9 ++- .../transformers/declarations/util.go | 13 --- .../reference/conformance/extendsTag10.js | 76 +++++++++++++++++ .../conformance/extendsTag10.symbols | 46 +++++++++++ .../reference/conformance/extendsTag10.types | 54 +++++++++++++ .../conformance/extendsTag11.errors.txt | 49 +++++++++++ .../conformance/extendsTag11.symbols | 70 ++++++++++++++++ .../reference/conformance/extendsTag11.types | 81 +++++++++++++++++++ .../conformance/extendsTag8.errors.txt | 5 +- .../reference/conformance/extendsTag8.types | 2 +- .../conformance/extendsTag9.errors.txt | 5 +- .../reference/conformance/extendsTag9.types | 8 +- .../cases/conformance/jsdoc/extendsTag10.ts | 27 +++++++ .../cases/conformance/jsdoc/extendsTag11.ts | 42 ++++++++++ 22 files changed, 585 insertions(+), 73 deletions(-) create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag10.js create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag10.symbols create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag10.types create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag11.errors.txt create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag11.symbols create mode 100644 tsc/testdata/baselines/reference/conformance/extendsTag11.types create mode 100644 tsc/testdata/tests/cases/conformance/jsdoc/extendsTag10.ts create mode 100644 tsc/testdata/tests/cases/conformance/jsdoc/extendsTag11.ts diff --git a/tsc/internal/ast/utilities.go b/tsc/internal/ast/utilities.go index 42058ea2c399f..d6f634a0885ba 100644 --- a/tsc/internal/ast/utilities.go +++ b/tsc/internal/ast/utilities.go @@ -1219,6 +1219,24 @@ func IsVarUsing(node *Node) bool { return GetCombinedNodeFlags(node)&NodeFlagsBlockScoped == NodeFlagsUsing } +// GetJSDocAugmentsTag returns the first @augments JSDoc tag for the given node, or nil if none exists. +func GetJSDocAugmentsTag(node *Node) *Node { + if node == nil { + return nil + } + for _, jsdoc := range node.JSDoc(nil) { + tags := jsdoc.AsJSDoc().Tags + if tags != nil { + for _, tag := range tags.Nodes { + if IsJSDocAugmentsTag(tag) { + return tag + } + } + } + } + return nil +} + // GetJSDocDeprecatedTag returns the first @deprecated JSDoc tag for the given node, or nil if none exists. func GetJSDocDeprecatedTag(node *Node) *Node { for _, jsdoc := range node.JSDoc(nil) { diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 0a3ae8a65dd41..9674c8d411441 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -4321,14 +4321,14 @@ func (c *Checker) checkClassLikeDeclaration(node *ast.Node) { c.checkClassForStaticPropertyNameConflicts(node) } - baseTypeNode := ast.GetClassExtendsHeritageElement(node) + baseTypeNode := c.getEffectiveBaseTypeNode(classType) if baseTypeNode != nil { c.checkSourceElements(baseTypeNode.TypeArguments()) baseTypes := c.getBaseTypes(classType) if len(baseTypes) != 0 { baseType := baseTypes[0] - c.checkJSDocAugmentsTagMatchesExtends(node, baseTypeNode, baseType) baseConstructorType := c.getBaseConstructorTypeOfClass(classType) + c.checkJSDocAugmentsTagMatchesExtends(node, baseTypeNode, baseType, baseConstructorType) staticBaseType := c.getApparentType(baseConstructorType) c.checkBaseTypeAccessibility(staticBaseType, baseTypeNode) c.checkSourceElement(baseTypeNode.Expression()) @@ -4403,41 +4403,6 @@ func (c *Checker) checkClassLikeDeclaration(node *ast.Node) { c.checkPropertyInitialization(node) } -func (c *Checker) checkJSDocAugmentsTagMatchesExtends(node *ast.Node, baseTypeNode *ast.ExpressionWithTypeArgumentsNode, baseType *Type) { - if !ast.IsInJSFile(node) { - return - } - file := ast.GetSourceFileOfNode(node) - for _, j := range node.EagerJSDoc(file) { - if j.AsJSDoc().Tags == nil { - continue - } - for _, tag := range j.AsJSDoc().Tags.Nodes { - if tag.Kind != ast.KindJSDocAugmentsTag { - continue - } - sourceTypeNode := tag.ClassName() - sourceType := c.getTypeFromTypeNode(sourceTypeNode) - sourceName := getIdentifierFromEntityNameExpression(sourceTypeNode.Expression()) - if sourceName != nil && ast.IsCallExpression(ast.SkipParentheses(baseTypeNode.Expression())) { - sourceSymbol := c.getMergedSymbol(getTargetType(sourceType).symbol) - targetSymbol := c.getMergedSymbol(getTargetType(baseType).symbol) - if sourceSymbol != nil && targetSymbol != nil && sourceSymbol != targetSymbol { - c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), targetSymbol.Name) - continue - } - } - if c.isTypeIdenticalTo(sourceType, baseType) { - continue - } - targetName := getIdentifierFromEntityNameExpression(baseTypeNode.Expression()) - if targetName != nil && sourceName != nil { - c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), targetName.Text()) - } - } - } -} - func (c *Checker) checkClassForStaticPropertyNameConflicts(node *ast.Node) { if c.compilerOptions.GetUseDefineForClassFields() { return @@ -8582,7 +8547,9 @@ func (c *Checker) resolveCallExpression(node *ast.Node, candidatesOutArray *[]*S if !c.isErrorType(superType) { // In super call, the candidate signatures are the matching arity signatures of the base constructor function instantiated // with the type arguments specified in the extends clause. - baseTypeNode := ast.GetClassExtendsHeritageElement(ast.GetContainingClass(node)) + containingClass := ast.GetContainingClass(node) + classType := c.getDeclaredTypeOfSymbol(c.getSymbolOfDeclaration(containingClass)) + baseTypeNode := c.getEffectiveBaseTypeNode(classType) if baseTypeNode != nil { baseConstructors := c.getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.TypeArguments(), baseTypeNode) return c.resolveCall(node, baseConstructors, candidatesOutArray, checkMode, SignatureFlagsNone, nil) @@ -19348,7 +19315,7 @@ func (c *Checker) resolveBaseTypesOfClass(t *Type) { if baseConstructorType.flags&(TypeFlagsObject|TypeFlagsIntersection|TypeFlagsAny) == 0 { return } - baseTypeNode := getBaseTypeNodeOfClass(t) + baseTypeNode := c.getEffectiveBaseTypeNode(t) var baseType *Type var originalBaseType *Type if baseConstructorType.symbol != nil { @@ -20989,7 +20956,7 @@ func (c *Checker) getDefaultConstructSignatures(classType *Type) []*Signature { flags := core.IfElse(isAbstract, SignatureFlagsConstruct|SignatureFlagsAbstract, SignatureFlagsConstruct) return []*Signature{c.newSignature(flags, nil, classType.AsInterfaceType().LocalTypeParameters(), nil, nil, classType, nil, 0)} } - baseTypeNode := getBaseTypeNodeOfClass(classType) + baseTypeNode := c.getEffectiveBaseTypeNode(classType) isJavaScript := declaration != nil && ast.IsInJSFile(declaration) typeArguments := c.getTypeArgumentsFromNode(baseTypeNode) typeArgCount := len(typeArguments) diff --git a/tsc/internal/checker/emitresolver.go b/tsc/internal/checker/emitresolver.go index 1a4f86696fd2a..d5ffcf0893f84 100644 --- a/tsc/internal/checker/emitresolver.go +++ b/tsc/internal/checker/emitresolver.go @@ -1328,3 +1328,14 @@ func (r *EmitResolver) IsThisPropertyAssignmentDeclarationRedundant(node *ast.No } return false } + +func (r *EmitResolver) GetEffectiveBaseTypeNode(node *ast.Node) *ast.Node { + r.checkerMu.Lock() + defer r.checkerMu.Unlock() + symbol := r.checker.getSymbolOfDeclaration(node) + if symbol == nil { + return ast.GetClassExtendsHeritageElement(node) + } + classType := r.checker.getDeclaredTypeOfSymbol(symbol) + return r.checker.getEffectiveBaseTypeNode(classType) +} diff --git a/tsc/internal/checker/jsdoc.go b/tsc/internal/checker/jsdoc.go index c8b5fb1adba44..2d9090ba6c359 100644 --- a/tsc/internal/checker/jsdoc.go +++ b/tsc/internal/checker/jsdoc.go @@ -98,3 +98,65 @@ func getAllJSDocTags(node *ast.Node) []*ast.Node { } return nil } + +func (c *Checker) getEffectiveBaseTypeNode(t *Type) *ast.Node { + baseTypeNode := getBaseTypeNodeOfClass(t) + if tag := c.tryGetMatchingJSDocAugmentsTag(t, baseTypeNode); tag != nil { + return tag.ClassName() + } + return baseTypeNode +} + +func (c *Checker) tryGetMatchingJSDocAugmentsTag(t *Type, baseTypeNode *ast.Node) *ast.Node { + if baseTypeNode == nil || !ast.IsInJSFile(baseTypeNode) || len(baseTypeNode.TypeArguments()) >= 1 { + return nil + } + expression := ast.SkipParentheses(baseTypeNode.Expression()) + if ast.IsCallExpression(expression) { + tag := ast.GetJSDocAugmentsTag(ast.GetClassLikeDeclarationOfSymbol(t.symbol)) + if tag == nil || len(tag.ClassName().TypeArguments()) == 0 { + return nil + } + baseConstructorType := c.getBaseConstructorTypeOfClass(t) + if baseConstructorType.flags&TypeFlagsIntersection == 0 { + sourceType := c.getTypeFromTypeNode(tag.ClassName()) + sourceSymbol := c.getMergedSymbol(getTargetType(sourceType).symbol) + baseSymbol := c.getMergedSymbol(c.getApparentType(baseConstructorType).symbol) + if sourceSymbol != nil && sourceSymbol == baseSymbol { + return tag + } + } + } + return nil +} + +func (c *Checker) checkJSDocAugmentsTagMatchesExtends(node *ast.Node, baseTypeNode *ast.ExpressionWithTypeArgumentsNode, baseType *Type, baseConstructorType *Type) { + if !ast.IsInJSFile(node) { + return + } + tag := ast.GetJSDocAugmentsTag(node) + if tag == nil { + return + } + sourceTypeNode := tag.ClassName() + sourceType := c.getTypeFromTypeNode(sourceTypeNode) + sourceSymbol := c.getMergedSymbol(getTargetType(sourceType).symbol) + sourceName := getIdentifierFromEntityNameExpression(sourceTypeNode.Expression()) + if sourceName != nil && ast.IsCallExpression(ast.SkipParentheses(baseTypeNode.Expression())) { + targetSymbol := c.getMergedSymbol(c.getApparentType(baseConstructorType).symbol) + if sourceSymbol != nil && targetSymbol != nil && sourceSymbol != targetSymbol { + declarationName := getIdentifierNameOfSymbolDeclaration(targetSymbol) + if declarationName != nil { + c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), declarationName.Text()) + return + } + } + } + if c.isTypeIdenticalTo(sourceType, baseType) { + return + } + targetName := getIdentifierFromEntityNameExpression(baseTypeNode.Expression()) + if targetName != nil && sourceName != nil { + c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), targetName.Text()) + } +} diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index 9c486a5bb90ab..8747a56cbd56c 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -36,6 +36,21 @@ func NewDiagnosticChainForNode(chain *ast.Diagnostic, node *ast.Node, message *d return NewDiagnosticForNode(node, message, args...) } +func getIdentifierNameOfSymbolDeclaration(symbol *ast.Symbol) *ast.Node { + if symbol == nil { + return nil + } + if name := ast.GetNameOfDeclaration(symbol.ValueDeclaration); name != nil && ast.IsIdentifier(name) { + return name + } + for _, declaration := range symbol.Declarations { + if name := ast.GetNameOfDeclaration(declaration); name != nil && ast.IsIdentifier(name) { + return name + } + } + return nil +} + func findInMap[K comparable, V any](m map[K]V, predicate func(V) bool) V { for _, value := range m { if predicate(value) { diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index e168543c602c6..1646f7b7799dc 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -308,7 +308,7 @@ function foo(options) {}` assert.Equal(t, scanner.GetTokenPosOfNode(typeNode, file, false /*includeJSDoc*/), strings.Index(sourceText, "{{")+1) } -func TestJSDocAugmentsCallHeritage(t *testing.T) { +func TestJSDocDoesNotAugmentCallHeritage(t *testing.T) { t.Parallel() sourceText := `/** @template T */ class A { @@ -337,11 +337,7 @@ class B extends A.extend() {}` assert.Equal(t, scanner.GetTextOfNode(baseType.Expression()), "A.extend()") typeArguments := baseType.TypeArguments() - assert.Equal(t, len(typeArguments), 1) - - typeArgument := typeArguments[0] - assert.Equal(t, typeArgument.Kind, ast.KindStringKeyword) - assert.Assert(t, typeArgument.Flags&ast.NodeFlagsReparsed != 0) + assert.Equal(t, len(typeArguments), 0) jsDocs := classB.JSDoc(file) assert.Equal(t, len(jsDocs), 1) @@ -355,7 +351,8 @@ class B extends A.extend() {}` sourceTypeArguments := tag.ClassName().TypeArguments() assert.Equal(t, len(sourceTypeArguments), 1) - assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), typeArgument) + assert.Equal(t, sourceTypeArguments[0].Kind, ast.KindStringKeyword) + assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), sourceTypeArguments[0]) } func TestSourceFilePositionMapWithNonASCIIStringLiteral(t *testing.T) { diff --git a/tsc/internal/parser/reparser.go b/tsc/internal/parser/reparser.go index 947afb69e613a..e5cc2438e9843 100644 --- a/tsc/internal/parser/reparser.go +++ b/tsc/internal/parser/reparser.go @@ -597,10 +597,8 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node) }); extendsClause != nil && len(extendsClause.AsHeritageClause().Types.Nodes) == 1 { target := extendsClause.AsHeritageClause().Types.Nodes[0].AsExpressionWithTypeArguments() source := tag.ClassName().AsExpressionWithTypeArguments() - if target.TypeArguments == nil && source.TypeArguments != nil { - if ast.IsCallExpression(ast.SkipParentheses(target.Expression)) || ast.HasSamePropertyAccessName(target.Expression, source.Expression) { - p.setReparsedTypeArguments(target, source.TypeArguments) - } + if target.TypeArguments == nil && source.TypeArguments != nil && ast.HasSamePropertyAccessName(target.Expression, source.Expression) { + p.setReparsedTypeArguments(target, source.TypeArguments) } } } diff --git a/tsc/internal/printer/emitresolver.go b/tsc/internal/printer/emitresolver.go index e6b60e801ed94..1b2e52d11d365 100644 --- a/tsc/internal/printer/emitresolver.go +++ b/tsc/internal/printer/emitresolver.go @@ -112,6 +112,7 @@ type EmitResolver interface { IsLateBound(node *ast.Node) bool IsOptionalParameter(node *ast.Node) bool IsThisPropertyAssignmentDeclarationRedundant(node *ast.Node) bool + GetEffectiveBaseTypeNode(node *ast.Node) *ast.Node // isolatedDeclarations-specific declaration emit GetPropertiesOfContainerFunction(node *ast.Node) []*ast.Symbol diff --git a/tsc/internal/transformers/declarations/transform.go b/tsc/internal/transformers/declarations/transform.go index 08e26b96a82fd..bb6cb6e226546 100644 --- a/tsc/internal/transformers/declarations/transform.go +++ b/tsc/internal/transformers/declarations/transform.go @@ -2002,9 +2002,14 @@ func (tx *DeclarationTransformer) transformClassDeclaration(input *ast.ClassDecl members := tx.buildClassMembers(input.AsNode(), extraMembers...) - extendsClause := getEffectiveBaseTypeNode(input.AsNode()) + extendsClause := ast.GetClassExtendsHeritageElement(input.AsNode()) if extendsClause != nil && !ast.IsEntityNameExpression(extendsClause.AsExpressionWithTypeArguments().Expression) && extendsClause.AsExpressionWithTypeArguments().Expression.Kind != ast.KindNullKeyword { + effectiveBaseType := tx.resolver.GetEffectiveBaseTypeNode(input.AsNode()) + typeArguments := extendsClause.AsExpressionWithTypeArguments().TypeArguments + if effectiveBaseType != nil { + typeArguments = effectiveBaseType.AsExpressionWithTypeArguments().TypeArguments + } tx.tracker.ReportInferenceFallback(extendsClause.AsExpressionWithTypeArguments().Expression) // Add an isolated declarations error on this extends clause oldId := "default" if ast.NodeIsPresent(input.Name()) && ast.IsIdentifier(input.Name()) && len(input.Name().Text()) > 0 { @@ -2040,7 +2045,7 @@ func (tx *DeclarationTransformer) transformClassDeclaration(input *ast.ClassDecl tx.Factory().UpdateExpressionWithTypeArguments( extendsClause.AsExpressionWithTypeArguments(), newId, - tx.Visitor().VisitNodes(extendsClause.AsExpressionWithTypeArguments().TypeArguments), + tx.Visitor().VisitNodes(typeArguments), ), }), ) diff --git a/tsc/internal/transformers/declarations/util.go b/tsc/internal/transformers/declarations/util.go index cfe66b26ba76c..b33e792d80b5b 100644 --- a/tsc/internal/transformers/declarations/util.go +++ b/tsc/internal/transformers/declarations/util.go @@ -161,19 +161,6 @@ func shouldEmitFunctionProperties(input *ast.FunctionDeclaration) bool { }) } -func getEffectiveBaseTypeNode(node *ast.Node) *ast.Node { - baseType := ast.GetClassExtendsHeritageElement(node) - // !!! TODO: JSDoc support - // if (baseType && isInJSFile(node)) { - // // Prefer an @augments tag because it may have type parameters. - // const tag = getJSDocAugmentsTag(node); - // if (tag) { - // return tag.class; - // } - // } - return baseType -} - func isScopeMarker(node *ast.Node) bool { return ast.IsExportAssignment(node) || ast.IsExportDeclaration(node) } diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag10.js b/tsc/testdata/baselines/reference/conformance/extendsTag10.js new file mode 100644 index 0000000000000..28ab2da1e2727 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag10.js @@ -0,0 +1,76 @@ +//// [tests/cases/conformance/jsdoc/extendsTag10.ts] //// + +//// [a.js] +/** @template T */ +class A { + /** @returns {T} */ + get value() { + throw new Error(); + } +} + +/** @param {any} Base */ +function mixin(Base) { + return class extends Base { + extra = 1; + }; +} + +/** @extends {A} */ +class B extends mixin(A) {} + +const value = new B().value; +const extra = new B().extra; + + +//// [a.js] +"use strict"; +/** @template T */ +class A { + /** @returns {T} */ + get value() { + throw new Error(); + } +} +/** @param {any} Base */ +function mixin(Base) { + return class extends Base { + constructor() { + super(...arguments); + this.extra = 1; + } + }; +} +/** @extends {A} */ +class B extends mixin(A) { +} +const value = new B().value; +const extra = new B().extra; + + +//// [a.d.ts] +/** @template T */ +declare class A { + /** @returns {T} */ + get value(): T; +} +/** @param {any} Base */ +declare function mixin(Base: any): { + new (): { + [x: string]: any; + extra: number; + }; + [x: string]: any; +}; +declare const B_base: { + new (): { + [x: string]: any; + extra: number; + }; + [x: string]: any; +}; +/** @extends {A} */ +declare class B extends B_base { +} +declare const value: any; +declare const extra: number; diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag10.symbols b/tsc/testdata/baselines/reference/conformance/extendsTag10.symbols new file mode 100644 index 0000000000000..2cb465b2039e2 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag10.symbols @@ -0,0 +1,46 @@ +//// [tests/cases/conformance/jsdoc/extendsTag10.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : Symbol(A, Decl(a.js, 0, 0)) + + /** @returns {T} */ + get value() { +>value : Symbol(A.value, Decl(a.js, 1, 9)) + + throw new Error(); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } +} + +/** @param {any} Base */ +function mixin(Base) { +>mixin : Symbol(mixin, Decl(a.js, 6, 1)) +>Base : Symbol(Base, Decl(a.js, 9, 15)) + + return class extends Base { +>Base : Symbol(Base, Decl(a.js, 9, 15)) + + extra = 1; +>extra : Symbol((Anonymous class).extra, Decl(a.js, 10, 31)) + + }; +} + +/** @extends {A} */ +class B extends mixin(A) {} +>B : Symbol(B, Decl(a.js, 13, 1)) +>mixin : Symbol(mixin, Decl(a.js, 6, 1)) +>A : Symbol(A, Decl(a.js, 0, 0)) + +const value = new B().value; +>value : Symbol(value, Decl(a.js, 18, 5)) +>B : Symbol(B, Decl(a.js, 13, 1)) + +const extra = new B().extra; +>extra : Symbol(extra, Decl(a.js, 19, 5)) +>new B().extra : Symbol((Anonymous class).extra, Decl(a.js, 10, 31)) +>B : Symbol(B, Decl(a.js, 13, 1)) +>extra : Symbol((Anonymous class).extra, Decl(a.js, 10, 31)) + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag10.types b/tsc/testdata/baselines/reference/conformance/extendsTag10.types new file mode 100644 index 0000000000000..5b9852dab2420 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag10.types @@ -0,0 +1,54 @@ +//// [tests/cases/conformance/jsdoc/extendsTag10.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : A + + /** @returns {T} */ + get value() { +>value : T + + throw new Error(); +>new Error() : Error +>Error : ErrorConstructor + } +} + +/** @param {any} Base */ +function mixin(Base) { +>mixin : (Base: any) => typeof (Anonymous class) +>Base : any + + return class extends Base { +>class extends Base { extra = 1; } : typeof (Anonymous class) +>Base : any + + extra = 1; +>extra : number +>1 : 1 + + }; +} + +/** @extends {A} */ +class B extends mixin(A) {} +>B : B +>mixin(A) : (Anonymous class) +>mixin : (Base: any) => typeof (Anonymous class) +>A : typeof A + +const value = new B().value; +>value : any +>new B().value : any +>new B() : B +>B : typeof B +>value : any + +const extra = new B().extra; +>extra : number +>new B().extra : number +>new B() : B +>B : typeof B +>extra : number + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag11.errors.txt b/tsc/testdata/baselines/reference/conformance/extendsTag11.errors.txt new file mode 100644 index 0000000000000..bd9db178f4833 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag11.errors.txt @@ -0,0 +1,49 @@ +a.js(18,7): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. +a.js(23,15): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. +a.js(36,22): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. + + +==== a.js (3 errors) ==== + /** @template T */ + class A { + /** @param {T} value */ + constructor(value) { + this.value = value; + } + + /** @returns {typeof A} */ + static extend() { + return this; + } + } + + /** @extends {A} */ + class B extends A.extend() {} + + new B("ok"); + new B(1); + ~ +!!! error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. + + /** @extends {A} */ + class C extends A.extend() { + constructor() { + super(1); + ~ +!!! error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. + } + } + + /** + * @param {number} required + * @returns {typeof A} + */ + function getA(required) { + return A; + } + + /** @extends {A} */ + class D extends getA("wrong") {} + ~~~~~~~ +!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag11.symbols b/tsc/testdata/baselines/reference/conformance/extendsTag11.symbols new file mode 100644 index 0000000000000..886f9ac64cf2f --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag11.symbols @@ -0,0 +1,70 @@ +//// [tests/cases/conformance/jsdoc/extendsTag11.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : Symbol(A, Decl(a.js, 0, 0)) + + /** @param {T} value */ + constructor(value) { +>value : Symbol(value, Decl(a.js, 3, 16)) + + this.value = value; +>this.value : Symbol(A.value, Decl(a.js, 3, 24)) +>this : Symbol(A, Decl(a.js, 0, 0)) +>value : Symbol(A.value, Decl(a.js, 3, 24)) +>value : Symbol(value, Decl(a.js, 3, 16)) + } + + /** @returns {typeof A} */ + static extend() { +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + + return this; +>this : Symbol(A, Decl(a.js, 0, 0)) + } +} + +/** @extends {A} */ +class B extends A.extend() {} +>B : Symbol(B, Decl(a.js, 11, 1)) +>A.extend : Symbol(A.extend, Decl(a.js, 5, 5)) +>A : Symbol(A, Decl(a.js, 0, 0)) +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + +new B("ok"); +>B : Symbol(B, Decl(a.js, 11, 1)) + +new B(1); +>B : Symbol(B, Decl(a.js, 11, 1)) + +/** @extends {A} */ +class C extends A.extend() { +>C : Symbol(C, Decl(a.js, 17, 9)) +>A.extend : Symbol(A.extend, Decl(a.js, 5, 5)) +>A : Symbol(A, Decl(a.js, 0, 0)) +>extend : Symbol(A.extend, Decl(a.js, 5, 5)) + + constructor() { + super(1); +>super : Symbol(A, Decl(a.js, 0, 0)) + } +} + +/** + * @param {number} required + * @returns {typeof A} + */ +function getA(required) { +>getA : Symbol(getA, Decl(a.js, 24, 1)) +>required : Symbol(required, Decl(a.js, 30, 14)) + + return A; +>A : Symbol(A, Decl(a.js, 0, 0)) +} + +/** @extends {A} */ +class D extends getA("wrong") {} +>D : Symbol(D, Decl(a.js, 32, 1)) +>getA : Symbol(getA, Decl(a.js, 24, 1)) + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag11.types b/tsc/testdata/baselines/reference/conformance/extendsTag11.types new file mode 100644 index 0000000000000..e0fb705e38eee --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/extendsTag11.types @@ -0,0 +1,81 @@ +//// [tests/cases/conformance/jsdoc/extendsTag11.ts] //// + +=== a.js === +/** @template T */ +class A { +>A : A + + /** @param {T} value */ + constructor(value) { +>value : T + + this.value = value; +>this.value = value : T +>this.value : any +>this : this +>value : any +>value : T + } + + /** @returns {typeof A} */ + static extend() { +>extend : () => typeof A + + return this; +>this : typeof A + } +} + +/** @extends {A} */ +class B extends A.extend() {} +>B : B +>A.extend() : A +>A.extend : () => typeof A +>A : typeof A +>extend : () => typeof A + +new B("ok"); +>new B("ok") : B +>B : typeof B +>"ok" : "ok" + +new B(1); +>new B(1) : B +>B : typeof B +>1 : 1 + +/** @extends {A} */ +class C extends A.extend() { +>C : C +>A.extend() : A +>A.extend : () => typeof A +>A : typeof A +>extend : () => typeof A + + constructor() { + super(1); +>super(1) : void +>super : typeof A +>1 : 1 + } +} + +/** + * @param {number} required + * @returns {typeof A} + */ +function getA(required) { +>getA : (required: number) => typeof A +>required : number + + return A; +>A : typeof A +} + +/** @extends {A} */ +class D extends getA("wrong") {} +>D : D +>getA("wrong") : A +>getA : (required: number) => typeof A +>"wrong" : "wrong" + diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt b/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt index a831aebd4bbd7..9512110af2b8c 100644 --- a/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.errors.txt @@ -1,7 +1,8 @@ a.js(12,15): error TS8023: JSDoc '@extends C' does not match the 'extends A' clause. +a.js(13,17): error TS8026: Expected A type arguments; provide these with an '@extends' tag. -==== a.js (1 errors) ==== +==== a.js (2 errors) ==== /** @template T */ class A { /** @returns {typeof A} */ @@ -17,4 +18,6 @@ a.js(12,15): error TS8023: JSDoc '@extends C' does not match the 'extends A' cla ~ !!! error TS8023: JSDoc '@extends C' does not match the 'extends A' clause. class B extends A.extend() {} + ~~~~~~~~~~ +!!! error TS8026: Expected A type arguments; provide these with an '@extends' tag. \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag8.types b/tsc/testdata/baselines/reference/conformance/extendsTag8.types index 4c71958d321c1..9c62961a7581e 100644 --- a/tsc/testdata/baselines/reference/conformance/extendsTag8.types +++ b/tsc/testdata/baselines/reference/conformance/extendsTag8.types @@ -21,7 +21,7 @@ class C {} /** @extends {C} */ class B extends A.extend() {} >B : B ->A.extend() : A +>A.extend() : A >A.extend : () => typeof A >A : typeof A >extend : () => typeof A diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt b/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt index 218b8ea0a3000..6c4a1fd1494de 100644 --- a/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt +++ b/tsc/testdata/baselines/reference/conformance/extendsTag9.errors.txt @@ -1,8 +1,9 @@ a.js(22,15): error TS8023: JSDoc '@extends A' does not match the 'extends C' clause. +a.js(23,17): error TS8026: Expected C type arguments; provide these with an '@extends' tag. a.js(25,19): error TS2339: Property 'a' does not exist on type 'B'. -==== a.js (2 errors) ==== +==== a.js (3 errors) ==== /** @template T */ class A { /** @returns {T} */ @@ -28,6 +29,8 @@ a.js(25,19): error TS2339: Property 'a' does not exist on type 'B'. ~ !!! error TS8023: JSDoc '@extends A' does not match the 'extends C' clause. class B extends A.extend() {} + ~~~~~~~~~~ +!!! error TS8026: Expected C type arguments; provide these with an '@extends' tag. const a = new B().a; ~ diff --git a/tsc/testdata/baselines/reference/conformance/extendsTag9.types b/tsc/testdata/baselines/reference/conformance/extendsTag9.types index 42d02b953dc31..bf6293901155a 100644 --- a/tsc/testdata/baselines/reference/conformance/extendsTag9.types +++ b/tsc/testdata/baselines/reference/conformance/extendsTag9.types @@ -40,7 +40,7 @@ class C { /** @extends {A} */ class B extends A.extend() {} >B : B ->A.extend() : C +>A.extend() : C >A.extend : () => typeof C >A : typeof A >extend : () => typeof C @@ -53,9 +53,9 @@ const a = new B().a; >a : any const c = new B().c; ->c : string ->new B().c : string +>c : any +>new B().c : any >new B() : B >B : typeof B ->c : string +>c : any diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag10.ts b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag10.ts new file mode 100644 index 0000000000000..93c7e3e57b711 --- /dev/null +++ b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag10.ts @@ -0,0 +1,27 @@ +// @target: es2015 +// @allowJs: true +// @checkJs: true +// @declaration: true +// @outDir: out + +// @filename: a.js +/** @template T */ +class A { + /** @returns {T} */ + get value() { + throw new Error(); + } +} + +/** @param {any} Base */ +function mixin(Base) { + return class extends Base { + extra = 1; + }; +} + +/** @extends {A} */ +class B extends mixin(A) {} + +const value = new B().value; +const extra = new B().extra; diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag11.ts b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag11.ts new file mode 100644 index 0000000000000..2d7b0d7779701 --- /dev/null +++ b/tsc/testdata/tests/cases/conformance/jsdoc/extendsTag11.ts @@ -0,0 +1,42 @@ +// @target: es2015 +// @allowJs: true +// @checkJs: true +// @noEmit: true + +// @filename: a.js +/** @template T */ +class A { + /** @param {T} value */ + constructor(value) { + this.value = value; + } + + /** @returns {typeof A} */ + static extend() { + return this; + } +} + +/** @extends {A} */ +class B extends A.extend() {} + +new B("ok"); +new B(1); + +/** @extends {A} */ +class C extends A.extend() { + constructor() { + super(1); + } +} + +/** + * @param {number} required + * @returns {typeof A} + */ +function getA(required) { + return A; +} + +/** @extends {A} */ +class D extends getA("wrong") {}