Skip to content

Commit 1a2de72

Browse files
author
Andy
authored
Fixes to @Augments handling (#18775)
* Fixes to @Augments handling * Renames and diagnostic changes * Add test for < > characters * Use more specific return type
1 parent 8e7a5ba commit 1a2de72

27 files changed

+350
-34
lines changed

src/compiler/checker.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4882,7 +4882,16 @@ namespace ts {
48824882
}
48834883

48844884
function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments {
4885-
return getClassExtendsHeritageClauseElement(<ClassLikeDeclaration>type.symbol.valueDeclaration);
4885+
const decl = <ClassLikeDeclaration>type.symbol.valueDeclaration;
4886+
if (isInJavaScriptFile(decl)) {
4887+
// Prefer an @augments tag because it may have type parameters.
4888+
const tag = getJSDocAugmentsTag(decl);
4889+
if (tag) {
4890+
return tag.class;
4891+
}
4892+
}
4893+
4894+
return getClassExtendsHeritageClauseElement(decl);
48864895
}
48874896

48884897
function getConstructorsForTypeArguments(type: Type, typeArgumentNodes: ReadonlyArray<TypeNode>, location: Node): Signature[] {
@@ -4986,15 +4995,6 @@ namespace ts {
49864995
baseType = getReturnTypeOfSignature(constructors[0]);
49874996
}
49884997

4989-
// In a JS file, you can use the @augments jsdoc tag to specify a base type with type parameters
4990-
const valueDecl = type.symbol.valueDeclaration;
4991-
if (valueDecl && isInJavaScriptFile(valueDecl)) {
4992-
const augTag = getJSDocAugmentsTag(type.symbol.valueDeclaration);
4993-
if (augTag && augTag.typeExpression && augTag.typeExpression.type) {
4994-
baseType = getTypeFromTypeNode(augTag.typeExpression.type);
4995-
}
4996-
}
4997-
49984998
if (baseType === unknownType) {
49994999
return;
50005000
}
@@ -5003,7 +5003,7 @@ namespace ts {
50035003
return;
50045004
}
50055005
if (type === baseType || hasBaseType(baseType, type)) {
5006-
error(valueDecl, Diagnostics.Type_0_recursively_references_itself_as_a_base_type,
5006+
error(type.symbol.valueDeclaration, Diagnostics.Type_0_recursively_references_itself_as_a_base_type,
50075007
typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType));
50085008
return;
50095009
}
@@ -19789,6 +19789,38 @@ namespace ts {
1978919789
}
1979019790
}
1979119791

19792+
function checkJSDocAugmentsTag(node: JSDocAugmentsTag): void {
19793+
const cls = getJSDocHost(node);
19794+
if (!isClassDeclaration(cls) && !isClassExpression(cls)) {
19795+
error(cls, Diagnostics.JSDoc_augments_is_not_attached_to_a_class_declaration);
19796+
return;
19797+
}
19798+
19799+
const name = getIdentifierFromEntityNameExpression(node.class.expression);
19800+
const extend = getClassExtendsHeritageClauseElement(cls);
19801+
if (extend) {
19802+
const className = getIdentifierFromEntityNameExpression(extend.expression);
19803+
if (className && name.escapedText !== className.escapedText) {
19804+
error(name, Diagnostics.JSDoc_augments_0_does_not_match_the_extends_1_clause,
19805+
unescapeLeadingUnderscores(name.escapedText),
19806+
unescapeLeadingUnderscores(className.escapedText));
19807+
}
19808+
}
19809+
}
19810+
19811+
function getIdentifierFromEntityNameExpression(node: Identifier | PropertyAccessExpression): Identifier;
19812+
function getIdentifierFromEntityNameExpression(node: Expression): Identifier | undefined;
19813+
function getIdentifierFromEntityNameExpression(node: Expression): Identifier | undefined {
19814+
switch (node.kind) {
19815+
case SyntaxKind.Identifier:
19816+
return node as Identifier;
19817+
case SyntaxKind.PropertyAccessExpression:
19818+
return (node as PropertyAccessExpression).name;
19819+
default:
19820+
return undefined;
19821+
}
19822+
}
19823+
1979219824
function checkFunctionOrMethodDeclaration(node: FunctionDeclaration | MethodDeclaration): void {
1979319825
checkDecorators(node);
1979419826
checkSignatureDeclaration(node);
@@ -22483,6 +22515,8 @@ namespace ts {
2248322515
case SyntaxKind.ParenthesizedType:
2248422516
case SyntaxKind.TypeOperator:
2248522517
return checkSourceElement((<ParenthesizedTypeNode | TypeOperatorNode>node).type);
22518+
case SyntaxKind.JSDocAugmentsTag:
22519+
return checkJSDocAugmentsTag(node as JSDocAugmentsTag);
2248622520
case SyntaxKind.JSDocTypedefTag:
2248722521
return checkJSDocTypedefTag(node as JSDocTypedefTag);
2248822522
case SyntaxKind.JSDocParameterTag:

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3511,6 +3511,14 @@
35113511
"category": "Error",
35123512
"code": 8021
35133513
},
3514+
"JSDoc '@augments' is not attached to a class declaration.": {
3515+
"category": "Error",
3516+
"code": 8022
3517+
},
3518+
"JSDoc '@augments {0}' does not match the 'extends {1}' clause.": {
3519+
"category": "Error",
3520+
"code": 8023
3521+
},
35143522
"Only identifiers/qualified-names with optional type arguments are currently supported in a class 'extends' clause.": {
35153523
"category": "Error",
35163524
"code": 9002

src/compiler/parser.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ namespace ts {
424424
case SyntaxKind.JSDocTypeTag:
425425
return visitNode(cbNode, (<JSDocTypeTag>node).typeExpression);
426426
case SyntaxKind.JSDocAugmentsTag:
427-
return visitNode(cbNode, (<JSDocAugmentsTag>node).typeExpression);
427+
return visitNode(cbNode, (<JSDocAugmentsTag>node).class);
428428
case SyntaxKind.JSDocTemplateTag:
429429
return visitNodes(cbNode, cbNodes, (<JSDocTemplateTag>node).typeParameters);
430430
case SyntaxKind.JSDocTypedefTag:
@@ -5624,13 +5624,16 @@ namespace ts {
56245624
function parseExpressionWithTypeArguments(): ExpressionWithTypeArguments {
56255625
const node = <ExpressionWithTypeArguments>createNode(SyntaxKind.ExpressionWithTypeArguments);
56265626
node.expression = parseLeftHandSideExpressionOrHigher();
5627-
if (token() === SyntaxKind.LessThanToken) {
5628-
node.typeArguments = parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken);
5629-
}
5630-
5627+
node.typeArguments = tryParseTypeArguments();
56315628
return finishNode(node);
56325629
}
56335630

5631+
function tryParseTypeArguments(): NodeArray<TypeNode> | undefined {
5632+
return token() === SyntaxKind.LessThanToken
5633+
? parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken)
5634+
: undefined;
5635+
}
5636+
56345637
function isHeritageClause(): boolean {
56355638
return token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword;
56365639
}
@@ -6604,15 +6607,36 @@ namespace ts {
66046607
}
66056608

66066609
function parseAugmentsTag(atToken: AtToken, tagName: Identifier): JSDocAugmentsTag {
6607-
const typeExpression = parseJSDocTypeExpression(/*requireBraces*/ true);
6608-
66096610
const result = <JSDocAugmentsTag>createNode(SyntaxKind.JSDocAugmentsTag, atToken.pos);
66106611
result.atToken = atToken;
66116612
result.tagName = tagName;
6612-
result.typeExpression = typeExpression;
6613+
result.class = parseExpressionWithTypeArgumentsForAugments();
66136614
return finishNode(result);
66146615
}
66156616

6617+
function parseExpressionWithTypeArgumentsForAugments(): ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression } {
6618+
const usedBrace = parseOptional(SyntaxKind.OpenBraceToken);
6619+
const node = createNode(SyntaxKind.ExpressionWithTypeArguments) as ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression };
6620+
node.expression = parsePropertyAccessEntityNameExpression();
6621+
node.typeArguments = tryParseTypeArguments();
6622+
const res = finishNode(node);
6623+
if (usedBrace) {
6624+
parseExpected(SyntaxKind.CloseBraceToken);
6625+
}
6626+
return res;
6627+
}
6628+
6629+
function parsePropertyAccessEntityNameExpression() {
6630+
let node: Identifier | PropertyAccessEntityNameExpression = parseJSDocIdentifierName(/*createIfMissing*/ true);
6631+
while (token() === SyntaxKind.DotToken) {
6632+
const prop: PropertyAccessEntityNameExpression = createNode(SyntaxKind.PropertyAccessExpression, node.pos) as PropertyAccessEntityNameExpression;
6633+
prop.expression = node;
6634+
prop.name = parseJSDocIdentifierName();
6635+
node = finishNode(prop);
6636+
}
6637+
return node;
6638+
}
6639+
66166640
function parseClassTag(atToken: AtToken, tagName: Identifier): JSDocClassTag {
66176641
const tag = <JSDocClassTag>createNode(SyntaxKind.JSDocClassTag, atToken.pos);
66186642
tag.atToken = atToken;

src/compiler/scanner.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,6 +1856,12 @@ namespace ts {
18561856
case CharacterCodes.closeBracket:
18571857
pos++;
18581858
return token = SyntaxKind.CloseBracketToken;
1859+
case CharacterCodes.lessThan:
1860+
pos++;
1861+
return token = SyntaxKind.LessThanToken;
1862+
case CharacterCodes.greaterThan:
1863+
pos++;
1864+
return token = SyntaxKind.GreaterThanToken;
18591865
case CharacterCodes.equals:
18601866
pos++;
18611867
return token = SyntaxKind.EqualsToken;

src/compiler/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2161,7 +2161,7 @@ namespace ts {
21612161

21622162
export interface JSDocAugmentsTag extends JSDocTag {
21632163
kind: SyntaxKind.JSDocAugmentsTag;
2164-
typeExpression: JSDocTypeExpression;
2164+
class: ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression };
21652165
}
21662166

21672167
export interface JSDocClassTag extends JSDocTag {

src/compiler/utilities.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,8 +1580,7 @@ namespace ts {
15801580
return undefined;
15811581
}
15821582
const name = node.name.escapedText;
1583-
Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment);
1584-
const func = node.parent!.parent!;
1583+
const func = getJSDocHost(node);
15851584
if (!isFunctionLike(func)) {
15861585
return undefined;
15871586
}
@@ -1590,6 +1589,11 @@ namespace ts {
15901589
return parameter && parameter.symbol;
15911590
}
15921591

1592+
export function getJSDocHost(node: JSDocTag): HasJSDoc {
1593+
Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment);
1594+
return node.parent!.parent!;
1595+
}
1596+
15931597
export function getTypeParameterFromJsDoc(node: TypeParameterDeclaration & { parent: JSDocTemplateTag }): TypeParameterDeclaration | undefined {
15941598
const name = node.name.escapedText;
15951599
const { typeParameters } = (node.parent.parent.parent as ts.SignatureDeclaration | ts.InterfaceDeclaration | ts.ClassDeclaration);

src/harness/unittests/jsDocParsing.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,11 @@ namespace ts {
300300
* @property {number} age
301301
* @property {string} name
302302
*/`);
303+
parsesCorrectly("<> characters",
304+
`/**
305+
* @param x hi
306+
< > still part of the previous comment
307+
*/`);
303308
});
304309
});
305310
describe("getFirstToken", () => {

src/services/completions.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,11 +581,10 @@ namespace ts.Completions {
581581

582582
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };
583583

584-
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
584+
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
585585

586586
function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression {
587587
switch (tag.kind) {
588-
case SyntaxKind.JSDocAugmentsTag:
589588
case SyntaxKind.JSDocParameterTag:
590589
case SyntaxKind.JSDocPropertyTag:
591590
case SyntaxKind.JSDocReturnTag:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"kind": "JSDocComment",
3+
"pos": 0,
4+
"end": 61,
5+
"tags": {
6+
"0": {
7+
"kind": "JSDocParameterTag",
8+
"pos": 7,
9+
"end": 16,
10+
"atToken": {
11+
"kind": "AtToken",
12+
"pos": 7,
13+
"end": 8
14+
},
15+
"tagName": {
16+
"kind": "Identifier",
17+
"pos": 8,
18+
"end": 13,
19+
"escapedText": "param"
20+
},
21+
"name": {
22+
"kind": "Identifier",
23+
"pos": 14,
24+
"end": 15,
25+
"escapedText": "x"
26+
},
27+
"isNameFirst": true,
28+
"isBracketed": false,
29+
"comment": "hi\n< > still part of the previous comment"
30+
},
31+
"length": 1,
32+
"pos": 7,
33+
"end": 16
34+
}
35+
}
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
/a.js(2,14): error TS1005: '{' expected.
1+
/a.js(2,14): error TS1003: Identifier expected.
2+
/a.js(2,14): error TS8023: JSDoc '@augments ' does not match the 'extends A' clause.
3+
/a.js(5,14): error TS2339: Property 'x' does not exist on type 'B'.
24

35

4-
==== /a.js (1 errors) ====
6+
==== /a.js (3 errors) ====
57
class A { constructor() { this.x = 0; } }
68
/** @augments */
7-
~
8-
!!! error TS1005: '{' expected.
9+
10+
!!! error TS1003: Identifier expected.
11+
12+
!!! error TS8023: JSDoc '@augments ' does not match the 'extends A' clause.
913
class B extends A {
1014
m() {
1115
this.x
16+
~
17+
!!! error TS2339: Property 'x' does not exist on type 'B'.
1218
}
1319
}
1420

tests/baselines/reference/jsdocAugmentsMissingType.symbols

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ class B extends A {
1414
>m : Symbol(B.m, Decl(a.js, 2, 19))
1515

1616
this.x
17-
>this.x : Symbol(A.x, Decl(a.js, 0, 25))
1817
>this : Symbol(B, Decl(a.js, 0, 41))
19-
>x : Symbol(A.x, Decl(a.js, 0, 25))
2018
}
2119
}
2220

tests/baselines/reference/jsdocAugmentsMissingType.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ class A { constructor() { this.x = 0; } }
1010
/** @augments */
1111
class B extends A {
1212
>B : B
13-
>A : A
13+
>A : typeof A
1414

1515
m() {
1616
>m : () => void
1717

1818
this.x
19-
>this.x : number
19+
>this.x : any
2020
>this : this
21-
>x : number
21+
>x : any
2222
}
2323
}
2424

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/b.js(4,15): error TS8023: JSDoc '@augments A' does not match the 'extends B' clause.
2+
3+
4+
==== /b.js (1 errors) ====
5+
class A {}
6+
class B {}
7+
8+
/** @augments A */
9+
~
10+
!!! error TS8023: JSDoc '@augments A' does not match the 'extends B' clause.
11+
class C extends B {}
12+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /b.js ===
2+
class A {}
3+
>A : Symbol(A, Decl(b.js, 0, 0))
4+
5+
class B {}
6+
>B : Symbol(B, Decl(b.js, 0, 10))
7+
8+
/** @augments A */
9+
class C extends B {}
10+
>C : Symbol(C, Decl(b.js, 1, 10))
11+
>B : Symbol(B, Decl(b.js, 0, 10))
12+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== /b.js ===
2+
class A {}
3+
>A : A
4+
5+
class B {}
6+
>B : B
7+
8+
/** @augments A */
9+
class C extends B {}
10+
>C : C
11+
>B : A
12+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
=== /b.js ===
2+
class A { constructor() { this.x = 0; } }
3+
>A : Symbol(A, Decl(b.js, 0, 0))
4+
>this.x : Symbol(A.x, Decl(b.js, 0, 25))
5+
>this : Symbol(A, Decl(b.js, 0, 0))
6+
>x : Symbol(A.x, Decl(b.js, 0, 25))
7+
8+
/** @augments A */
9+
class B {
10+
>B : Symbol(B, Decl(b.js, 0, 41))
11+
12+
m() {
13+
>m : Symbol(B.m, Decl(b.js, 3, 9))
14+
15+
return this.x;
16+
>this.x : Symbol(A.x, Decl(b.js, 0, 25))
17+
>this : Symbol(B, Decl(b.js, 0, 41))
18+
>x : Symbol(A.x, Decl(b.js, 0, 25))
19+
}
20+
}
21+

0 commit comments

Comments
 (0)