Skip to content
Merged
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
28 changes: 1 addition & 27 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4362,8 +4362,8 @@ func (c *Checker) checkClassLikeDeclaration(node *ast.Node) {
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)
staticBaseType := c.getApparentType(baseConstructorType)
c.checkBaseTypeAccessibility(staticBaseType, baseTypeNode)
c.checkSourceElement(baseTypeNode.Expression())
Expand Down Expand Up @@ -4438,32 +4438,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()
if c.isTypeIdenticalTo(c.getTypeFromTypeNode(sourceTypeNode), 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())
}
}
}
}

func (c *Checker) checkClassForStaticPropertyNameConflicts(node *ast.Node) {
if c.compilerOptions.GetUseDefineForClassFields() {
return
Expand Down
32 changes: 32 additions & 0 deletions tsc/internal/checker/jsdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/collections"
"github.com/microsoft/TypeScript/tsc/internal/diagnostics"
"github.com/microsoft/TypeScript/tsc/internal/scanner"
)

func (c *Checker) checkUnmatchedJSDocParameters(node *ast.Node) {
Expand Down Expand Up @@ -98,3 +99,34 @@ func getAllJSDocTags(node *ast.Node) []*ast.Node {
}
return nil
}

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 ast.IsEntityNameExpression(baseTypeNode.Expression()) {
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())
}
} else if sourceName != nil && !c.isTypeAssignableTo(baseType, sourceType) {
c.error(sourceName, diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, tag.TagName().Text(), sourceName.Text(), scanner.GetTextOfNode(baseTypeNode.Expression()))
}
}
}
}
63 changes: 63 additions & 0 deletions tsc/internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,69 @@ function foo(options) {}`
assert.Equal(t, scanner.GetTokenPosOfNode(typeNode, file, false /*includeJSDoc*/), strings.Index(sourceText, "{{")+1)
}

func TestJSDocAugmentsHeritage(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
expression string
tagName string
augments bool
}{
{"identifier", "A", "A", true},
{"dotted name", "ns.A", "ns.A", true},
{"call", "A.extend()", "A", true},
{"mixin", "mixin(A)", "A", true},
{"element access", `ns["A"]`, "A", true},
{"conditional", "(cond ? A : A)", "A", true},
{"parenthesized name", "(A)", "A", true},
{"null", "null", "A", false},
{"mismatched identifier", "A", "Other", false},
{"mismatched dotted name", "ns.A", "ns.Other", false},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
sourceText := "/** @extends {" + test.tagName + "<string>} */\nclass B extends " + test.expression + " {}"
opts := ast.SourceFileParseOptions{
FileName: "/index.js",
PathKey: "/index.js",
}
file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS)
parsetestutil.CheckDiagnostics(t, file)
assert.Equal(t, len(file.Statements.Nodes), 1)
classB := file.Statements.Nodes[0]
assert.Assert(t, ast.IsClassDeclaration(classB))

baseType := ast.GetClassExtendsHeritageElement(classB)
assert.Assert(t, baseType != nil)
assert.Equal(t, scanner.GetTextOfNode(baseType.Expression()), test.expression)

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)

typeArguments := baseType.TypeArguments()
if test.augments {
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, typeArgument.Parent, baseType)
assert.Equal(t, scanner.GetTextOfNode(typeArgument), "string")
assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), typeArgument)
} else {
assert.Equal(t, len(typeArguments), 0)
assert.Equal(t, ast.GetReparsedNodeForNode(sourceTypeArguments[0]), sourceTypeArguments[0])
}
})
}
}

func TestSourceFilePositionMapWithNonASCIIStringLiteral(t *testing.T) {
t.Parallel()
sourceText := `const x = "─";
Expand Down
20 changes: 11 additions & 9 deletions tsc/internal/parser/reparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,21 +597,23 @@ 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 && target.Expression.Kind != ast.KindNullKeyword && (!ast.IsEntityNameExpression(target.Expression) || 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
2 changes: 1 addition & 1 deletion tsc/internal/transformers/declarations/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2007,7 +2007,7 @@ 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 {
tx.tracker.ReportInferenceFallback(extendsClause.AsExpressionWithTypeArguments().Expression) // Add an isolated declarations error on this extends clause
Expand Down
13 changes: 0 additions & 13 deletions tsc/internal/transformers/declarations/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
a.js(17,17): error TS2315: Type '(Anonymous class)' is not generic.
a.js(19,23): error TS2339: Property 'value' does not exist on type 'B'.
a.js(20,23): error TS2339: Property 'extra' does not exist on type 'B'.


==== a.js (3 errors) ====
/** @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<string>} */
class B extends mixin(A) {}
~~~~~~~~
!!! error TS2315: Type '(Anonymous class)' is not generic.

const value = new B().value;
~~~~~
!!! error TS2339: Property 'value' does not exist on type 'B'.
const extra = new B().extra;
~~~~~
!!! error TS2339: Property 'extra' does not exist on type 'B'.

76 changes: 76 additions & 0 deletions tsc/testdata/baselines/reference/conformance/extendsTag10.js
Original file line number Diff line number Diff line change
@@ -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<string>} */
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<string>} */
class B extends mixin(A) {
}
const value = new B().value;
const extra = new B().extra;


//// [a.d.ts]
/** @template T */
declare class A<T> {
/** @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<string>} */
declare class B extends B_base<string> {
}
declare const value: any;
declare const extra: any;
44 changes: 44 additions & 0 deletions tsc/testdata/baselines/reference/conformance/extendsTag10.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//// [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<string>} */
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))
>B : Symbol(B, Decl(a.js, 13, 1))

Loading
Loading