Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions tsc/internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} */
class B 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), 2)

classB := statements[1]
assert.Assert(t, ast.IsClassDeclaration(classB))

baseType := ast.GetClassExtendsHeritageElement(classB)
assert.Assert(t, baseType != nil)

baseExpression := baseType.Expression()
assert.Assert(t, ast.IsAsExpression(baseExpression))

callExpression := baseExpression.Expression()
assert.Assert(t, ast.IsCallExpression(callExpression))
assert.Equal(t, scanner.GetTextOfNode(callExpression), "A.extend()")

typeQuery := baseExpression.Type()
assert.Equal(t, typeQuery.Kind, ast.KindTypeQuery)
assert.Assert(t, typeQuery.Flags&ast.NodeFlagsReparsed != 0)

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))
assert.Equal(t, ast.GetReparsedNodeForNode(tag.ClassName().Expression()), typeQuery.AsTypeQueryNode().ExprName)

sourceTypeArguments := tag.ClassName().TypeArguments()
assert.Equal(t, len(sourceTypeArguments), 1)
assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), typeArgument)
}

func TestSourceFilePositionMapWithNonASCIIStringLiteral(t *testing.T) {
t.Parallel()
sourceText := `const x = "─";
Expand Down
25 changes: 17 additions & 8 deletions tsc/internal/parser/reparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,21 +597,30 @@ 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)) {
typeQuery := p.factory.NewTypeQueryNode(p.addDeepCloneReparse(source.Expression), nil)
p.finishReparsedNode(typeQuery, source.Expression)
target.Expression = p.makeNewCast(typeQuery, target.Expression, true /*isAssertion*/)
p.setReparsedTypeArguments(target, source.TypeArguments)
} else if ast.HasSamePropertyAccessName(target.Expression, source.Expression) {
p.setReparsedTypeArguments(target, source.TypeArguments)
}
}
}
}
}
}

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 {
Expand Down
37 changes: 37 additions & 0 deletions tsc/testdata/baselines/reference/conformance/extendsTag7.symbols
Original file line number Diff line number Diff line change
@@ -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<string>} */
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))

40 changes: 40 additions & 0 deletions tsc/testdata/baselines/reference/conformance/extendsTag7.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//// [tests/cases/conformance/jsdoc/extendsTag7.ts] ////

=== a.js ===
/** @template T */
class A {
>A : A<T>

/** @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<string>} */
class B extends A.extend() {}
>B : B
>A.extend() : A<string>
>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

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
a.js(13,17): error TS8026: Expected A<T> 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<string>} */
class B extends A.extend() {}
~~~~~~~~~~
!!! error TS8026: Expected A<T> type arguments; provide these with an '@extends' tag.

27 changes: 27 additions & 0 deletions tsc/testdata/baselines/reference/conformance/extendsTag8.symbols
Original file line number Diff line number Diff line change
@@ -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<string>} */
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))

28 changes: 28 additions & 0 deletions tsc/testdata/baselines/reference/conformance/extendsTag8.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// [tests/cases/conformance/jsdoc/extendsTag8.ts] ////

=== a.js ===
/** @template T */
class A {
>A : A<T>

/** @returns {typeof A} */
static extend() {
>extend : () => typeof A

return this;
>this : typeof A
}
}

/** @template T */
class C {}
>C : C<T>

/** @extends {C<string>} */
class B extends A.extend() {}
>B : B
>A.extend() : A<any>
>A.extend : () => typeof A
>A : typeof A
>extend : () => typeof A

23 changes: 23 additions & 0 deletions tsc/testdata/tests/cases/conformance/jsdoc/extendsTag7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @target: es2015
// @allowJs: true
// @checkJs: true
// @declaration: true

// @filename: a.js
/** @template T */
class A {
/** @returns {T} */
get value() {
throw new Error();
}

/** @returns {typeof A} */
static extend() {
return this;
}
}

/** @extends {A<string>} */
class B extends A.extend() {}

const value = new B().value;
19 changes: 19 additions & 0 deletions tsc/testdata/tests/cases/conformance/jsdoc/extendsTag8.ts
Original file line number Diff line number Diff line change
@@ -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<string>} */
class B extends A.extend() {}
32 changes: 32 additions & 0 deletions tsc/testdata/tests/cases/conformance/jsdoc/extendsTag9.ts
Original file line number Diff line number Diff line change
@@ -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<string>} */
class B extends A.extend() {}

const a = new B().a;
const c = new B().c;
Loading