diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 718fd76625f01..aa5d10c475c8f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20009,14 +20009,20 @@ namespace ts { } function checkJSDocAugmentsTag(node: JSDocAugmentsTag): void { - const cls = getJSDocHost(node); - if (!isClassDeclaration(cls) && !isClassExpression(cls)) { - error(cls, Diagnostics.JSDoc_augments_is_not_attached_to_a_class_declaration); + const classLike = getJSDocHost(node); + if (!isClassDeclaration(classLike) && !isClassExpression(classLike)) { + error(classLike, Diagnostics.JSDoc_augments_is_not_attached_to_a_class_declaration); return; } + const augmentsTags = getAllJSDocTagsOfKind(classLike, SyntaxKind.JSDocAugmentsTag); + Debug.assert(augmentsTags.length > 0); + if (augmentsTags.length > 1) { + error(augmentsTags[1], Diagnostics.Class_declarations_cannot_have_more_than_one_augments_or_extends_tag); + } + const name = getIdentifierFromEntityNameExpression(node.class.expression); - const extend = getClassExtendsHeritageClauseElement(cls); + const extend = getClassExtendsHeritageClauseElement(classLike); if (extend) { const className = getIdentifierFromEntityNameExpression(extend.expression); if (className && name.escapedText !== className.escapedText) { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f3d6d4fcc4706..e0de43a97db6c 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3527,6 +3527,10 @@ "category": "Error", "code": 8024 }, + "Class declarations cannot have more than one `@augments` or `@extends` tag.": { + "category": "Error", + "code": 8025 + }, "Only identifiers/qualified-names with optional type arguments are currently supported in a class 'extends' clause.": { "category": "Error", "code": 9002 diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index ebdf390f5b262..5900fee5790e4 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -6373,6 +6373,7 @@ namespace ts { if (tagName) { switch (tagName.escapedText) { case "augments": + case "extends": tag = parseAugmentsTag(atToken, tagName); break; case "class": diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 578c2d23c4f39..3314b3a2fbce6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2159,6 +2159,10 @@ namespace ts { kind: SyntaxKind.JSDocTag; } + /** + * Note that `@extends` is a synonym of `@augments`. + * Both tags are represented by this interface. + */ export interface JSDocAugmentsTag extends JSDocTag { kind: SyntaxKind.JSDocAugmentsTag; class: ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression }; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f0eb394adb7dd..825b310ddeb8b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4247,6 +4247,12 @@ namespace ts { return find(tags, doc => doc.kind === kind); } + /** Gets all JSDoc tags of a specified kind, or undefined if not present. */ + export function getAllJSDocTagsOfKind(node: Node, kind: SyntaxKind): ReadonlyArray | undefined { + const tags = getJSDocTags(node); + return filter(tags, doc => doc.kind === kind); + } + } // Simple node tests of the form `node.kind === SyntaxKind.Foo`. diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 3bb2ed1167488..5f4ef1dbdfec4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1442,6 +1442,10 @@ declare namespace ts { interface JSDocUnknownTag extends JSDocTag { kind: SyntaxKind.JSDocTag; } + /** + * Note that `@extends` is a synonym of `@augments`. + * Both tags are represented by this interface. + */ interface JSDocAugmentsTag extends JSDocTag { kind: SyntaxKind.JSDocAugmentsTag; class: ExpressionWithTypeArguments & { @@ -2866,6 +2870,8 @@ declare namespace ts { function getJSDocReturnType(node: Node): TypeNode | undefined; /** Get all JSDoc tags related to a node, including those on parent nodes. */ function getJSDocTags(node: Node): ReadonlyArray | undefined; + /** Gets all JSDoc tags of a specified kind, or undefined if not present. */ + function getAllJSDocTagsOfKind(node: Node, kind: SyntaxKind): ReadonlyArray | undefined; } declare namespace ts { function isNumericLiteral(node: Node): node is NumericLiteral; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d41db2eb413ee..e608c7ffc3d1e 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1442,6 +1442,10 @@ declare namespace ts { interface JSDocUnknownTag extends JSDocTag { kind: SyntaxKind.JSDocTag; } + /** + * Note that `@extends` is a synonym of `@augments`. + * Both tags are represented by this interface. + */ interface JSDocAugmentsTag extends JSDocTag { kind: SyntaxKind.JSDocAugmentsTag; class: ExpressionWithTypeArguments & { @@ -2921,6 +2925,8 @@ declare namespace ts { function getJSDocReturnType(node: Node): TypeNode | undefined; /** Get all JSDoc tags related to a node, including those on parent nodes. */ function getJSDocTags(node: Node): ReadonlyArray | undefined; + /** Gets all JSDoc tags of a specified kind, or undefined if not present. */ + function getAllJSDocTagsOfKind(node: Node, kind: SyntaxKind): ReadonlyArray | undefined; } declare namespace ts { function isNumericLiteral(node: Node): node is NumericLiteral; diff --git a/tests/cases/fourslash/jsDocAugments.ts b/tests/cases/fourslash/jsDocAugments.ts index 1938cd0e2aaaa..cd2190e548683 100644 --- a/tests/cases/fourslash/jsDocAugments.ts +++ b/tests/cases/fourslash/jsDocAugments.ts @@ -20,4 +20,3 @@ goTo.marker(); verify.quickInfoIs("(local var) x: string"); - diff --git a/tests/cases/fourslash/jsDocAugmentsAndExtends.ts b/tests/cases/fourslash/jsDocAugmentsAndExtends.ts new file mode 100644 index 0000000000000..10f33260268ac --- /dev/null +++ b/tests/cases/fourslash/jsDocAugmentsAndExtends.ts @@ -0,0 +1,37 @@ +/// + +// @allowJs: true +// @checkJs: true +// @Filename: dummy.js + +//// /** +//// * @augments {Thing} +//// * @extends {Thing} +//// */ +//// class MyStringThing extends Thing { +//// constructor() { +//// super(); +//// var x = this.mine; +//// x/**/; +//// } +//// } + +// @Filename: declarations.d.ts +//// declare class Thing { +//// mine: T; +//// } + +// if more than one tag is present, report an error and take the type of the first entry. + +goTo.marker(); +verify.quickInfoIs("(local var) x: number"); +verify.getSemanticDiagnostics( +`[ + { + "message": "Class declarations cannot have more than one \`@augments\` or \`@extends\` tag.", + "start": 36, + "length": 24, + "category": "error", + "code": 8025 + } +]`); \ No newline at end of file diff --git a/tests/cases/fourslash/jsDocExtends.ts b/tests/cases/fourslash/jsDocExtends.ts new file mode 100644 index 0000000000000..6bce55695333a --- /dev/null +++ b/tests/cases/fourslash/jsDocExtends.ts @@ -0,0 +1,22 @@ +/// + +// @allowJs: true +// @Filename: dummy.js + +//// /** +//// * @extends {Thing} +//// */ +//// class MyStringThing extends Thing { +//// constructor() { +//// var x = this.mine; +//// x/**/; +//// } +//// } + +// @Filename: declarations.d.ts +//// declare class Thing { +//// mine: T; +//// } + +goTo.marker(); +verify.quickInfoIs("(local var) x: string");