diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 5c835582da..2dbfa6bd40 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4986,10 +4986,10 @@ namespace ts { baseType = getReturnTypeOfSignature(constructors[0]); } - // In a JS file, you can use the @augments jsdoc tag to specify a base type with type parameters + // In a JS file, you can use the @augments and @extends jsdoc tags to specify a base type with type parameters const valueDecl = type.symbol.valueDeclaration; if (valueDecl && isInJavaScriptFile(valueDecl)) { - const augTag = getJSDocAugmentsTag(type.symbol.valueDeclaration); + const augTag = getJSDocAugmentsOrExtendsTag(type.symbol.valueDeclaration); if (augTag) { baseType = getTypeFromTypeNode(augTag.typeExpression.type); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 3809a07142..7e9ac8c580 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -423,8 +423,9 @@ namespace ts { return visitNode(cbNode, (node).typeExpression); case SyntaxKind.JSDocTypeTag: return visitNode(cbNode, (node).typeExpression); - case SyntaxKind.JSDocAugmentsTag: - return visitNode(cbNode, (node).typeExpression); + case SyntaxKind.JSDocAugmentsOrExtendsTag: + case SyntaxKind.JSDocExtendsTag: + return visitNode(cbNode, (node).typeExpression); case SyntaxKind.JSDocTemplateTag: return visitNodes(cbNode, cbNodes, (node).typeParameters); case SyntaxKind.JSDocTypedefTag: @@ -6366,7 +6367,8 @@ namespace ts { if (tagName) { switch (tagName.escapedText) { case "augments": - tag = parseAugmentsTag(atToken, tagName); + case "extends": + tag = parseAugmentsOrExtendsTag(atToken, tagName); break; case "class": case "constructor": @@ -6603,10 +6605,10 @@ namespace ts { return finishNode(result); } - function parseAugmentsTag(atToken: AtToken, tagName: Identifier): JSDocAugmentsTag { + function parseAugmentsOrExtendsTag(atToken: AtToken, tagName: Identifier): JSDocAugmentsOrExtendsTag { const typeExpression = tryParseTypeExpression(); - const result = createNode(SyntaxKind.JSDocAugmentsTag, atToken.pos); + const result = createNode(SyntaxKind.JSDocAugmentsOrExtendsTag, atToken.pos); result.atToken = atToken; result.tagName = tagName; result.typeExpression = typeExpression; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4e5ca9f07e..e8f5423679 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -363,7 +363,8 @@ namespace ts { JSDocVariadicType, JSDocComment, JSDocTag, - JSDocAugmentsTag, + JSDocAugmentsOrExtendsTag, + JSDocExtendsTag, JSDocClassTag, JSDocParameterTag, JSDocReturnTag, @@ -2159,8 +2160,12 @@ namespace ts { kind: SyntaxKind.JSDocTag; } - export interface JSDocAugmentsTag extends JSDocTag { - kind: SyntaxKind.JSDocAugmentsTag; + /** + * Note that `@extends` is a synonym of `@augments`. + * Both are covered by this interface. + */ + export interface JSDocAugmentsOrExtendsTag extends JSDocTag { + kind: SyntaxKind.JSDocAugmentsOrExtendsTag; typeExpression: JSDocTypeExpression; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 674215b058..f9a0c4c38a 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4072,8 +4072,8 @@ namespace ts { } /** Gets the JSDoc augments tag for the node if present */ - export function getJSDocAugmentsTag(node: Node): JSDocAugmentsTag | undefined { - return getFirstJSDocTag(node, SyntaxKind.JSDocAugmentsTag) as JSDocAugmentsTag; + export function getJSDocAugmentsOrExtendsTag(node: Node): JSDocAugmentsOrExtendsTag | undefined { + return getFirstJSDocTag(node, SyntaxKind.JSDocAugmentsOrExtendsTag) as JSDocAugmentsOrExtendsTag; } /** Gets the JSDoc class tag for the node if present */ @@ -4765,8 +4765,8 @@ namespace ts { return node.kind === SyntaxKind.JSDocComment; } - export function isJSDocAugmentsTag(node: Node): node is JSDocAugmentsTag { - return node.kind === SyntaxKind.JSDocAugmentsTag; + export function isJSDocAugmentsOrExtendsTag(node: Node): node is JSDocAugmentsOrExtendsTag { + return node.kind === SyntaxKind.JSDocAugmentsOrExtendsTag; } export function isJSDocParameterTag(node: Node): node is JSDocParameterTag { diff --git a/src/services/completions.ts b/src/services/completions.ts index e271ef1210..a954f687cb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -581,11 +581,11 @@ namespace ts.Completions { return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters }; - type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; + type JSDocTagWithTypeExpression = JSDocAugmentsOrExtendsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression { switch (tag.kind) { - case SyntaxKind.JSDocAugmentsTag: + case SyntaxKind.JSDocAugmentsOrExtendsTag: case SyntaxKind.JSDocParameterTag: case SyntaxKind.JSDocPropertyTag: case SyntaxKind.JSDocReturnTag: diff --git a/tests/baselines/reference/APISample_jsdoc.js b/tests/baselines/reference/APISample_jsdoc.js index c74e188f38..33857d06a6 100644 --- a/tests/baselines/reference/APISample_jsdoc.js +++ b/tests/baselines/reference/APISample_jsdoc.js @@ -101,7 +101,7 @@ function getAllTags(node: ts.Node) { function getSomeOtherTags(node: ts.Node) { const tags: (ts.JSDocTag | undefined)[] = []; - tags.push(ts.getJSDocAugmentsTag(node)); + tags.push(ts.getJSDocAugmentsOrExtendsTag(node)); tags.push(ts.getJSDocClassTag(node)); tags.push(ts.getJSDocReturnTag(node)); const type = ts.getJSDocTypeTag(node); @@ -200,7 +200,7 @@ function getAllTags(node) { } function getSomeOtherTags(node) { var tags = []; - tags.push(ts.getJSDocAugmentsTag(node)); + tags.push(ts.getJSDocAugmentsOrExtendsTag(node)); tags.push(ts.getJSDocClassTag(node)); tags.push(ts.getJSDocReturnTag(node)); var type = ts.getJSDocTypeTag(node); diff --git a/tests/cases/compiler/APISample_jsdoc.ts b/tests/cases/compiler/APISample_jsdoc.ts index 70b814ffff..491ff2b7a2 100644 --- a/tests/cases/compiler/APISample_jsdoc.ts +++ b/tests/cases/compiler/APISample_jsdoc.ts @@ -1,116 +1,116 @@ -// @module: commonjs -// @includebuiltfile: typescript_standalone.d.ts -// @strict:true - -/* - * Note: This test is a public API sample. The original sources can be found - * at: https://github.com/YousefED/typescript-json-schema - * https://github.com/vega/ts-json-schema-generator - * Please log a "breaking change" issue for any API breaking change affecting this issue - */ - -declare var console: any; - -import * as ts from "typescript"; - -// excerpted from https://github.com/YousefED/typescript-json-schema -// (converted from a method and modified; for example, `this: any` to compensate, among other changes) -function parseCommentsIntoDefinition(this: any, - symbol: ts.Symbol, - definition: {description?: string, [s: string]: string | undefined}, - otherAnnotations: { [s: string]: true}): void { - if (!symbol) { - return; - } - - // the comments for a symbol - let comments = symbol.getDocumentationComment(); - - if (comments.length) { - definition.description = comments.map(comment => comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n")).join(""); - } - - // jsdocs are separate from comments - const jsdocs = symbol.getJsDocTags(); - jsdocs.forEach(doc => { - // if we have @TJS-... annotations, we have to parse them - const { name, text } = doc; - if (this.userValidationKeywords[name]) { - definition[name] = this.parseValue(text); - } else { - // special annotations - otherAnnotations[doc.name] = true; - } - }); -} - - -// excerpted from https://github.com/vega/ts-json-schema-generator -export interface Annotations { - [name: string]: any; -} -function getAnnotations(this: any, node: ts.Node): Annotations | undefined { - const symbol: ts.Symbol = (node as any).symbol; - if (!symbol) { - return undefined; - } - - const jsDocTags: ts.JSDocTagInfo[] = symbol.getJsDocTags(); - if (!jsDocTags || !jsDocTags.length) { - return undefined; - } - - const annotations: Annotations = jsDocTags.reduce((result: Annotations, jsDocTag: ts.JSDocTagInfo) => { - const value = this.parseJsDocTag(jsDocTag); - if (value !== undefined) { - result[jsDocTag.name] = value; - } - - return result; - }, {}); - return Object.keys(annotations).length ? annotations : undefined; -} - -// these examples are artificial and mostly nonsensical -function parseSpecificTags(node: ts.Node) { - if (node.kind === ts.SyntaxKind.Parameter) { - return ts.getJSDocParameterTags(node as ts.ParameterDeclaration); - } - if (node.kind === ts.SyntaxKind.FunctionDeclaration) { - const func = node as ts.FunctionDeclaration; - if (ts.hasJSDocParameterTags(func)) { - const flat: ts.JSDocTag[] = []; - for (const tags of func.parameters.map(ts.getJSDocParameterTags)) { - if (tags) flat.push(...tags); - } - return flat; - } - } -} - -function getReturnTypeFromJSDoc(node: ts.Node) { - if (node.kind === ts.SyntaxKind.FunctionDeclaration) { - return ts.getJSDocReturnType(node); - } - let type = ts.getJSDocType(node); - if (type && type.kind === ts.SyntaxKind.FunctionType) { - return (type as ts.FunctionTypeNode).type; - } -} - -function getAllTags(node: ts.Node) { - ts.getJSDocTags(node); -} - -function getSomeOtherTags(node: ts.Node) { - const tags: (ts.JSDocTag | undefined)[] = []; - tags.push(ts.getJSDocAugmentsTag(node)); - tags.push(ts.getJSDocClassTag(node)); - tags.push(ts.getJSDocReturnTag(node)); - const type = ts.getJSDocTypeTag(node); - if (type) { - tags.push(type); - } - tags.push(ts.getJSDocTemplateTag(node)); - return tags; -} +// @module: commonjs +// @includebuiltfile: typescript_standalone.d.ts +// @strict:true + +/* + * Note: This test is a public API sample. The original sources can be found + * at: https://github.com/YousefED/typescript-json-schema + * https://github.com/vega/ts-json-schema-generator + * Please log a "breaking change" issue for any API breaking change affecting this issue + */ + +declare var console: any; + +import * as ts from "typescript"; + +// excerpted from https://github.com/YousefED/typescript-json-schema +// (converted from a method and modified; for example, `this: any` to compensate, among other changes) +function parseCommentsIntoDefinition(this: any, + symbol: ts.Symbol, + definition: {description?: string, [s: string]: string | undefined}, + otherAnnotations: { [s: string]: true}): void { + if (!symbol) { + return; + } + + // the comments for a symbol + let comments = symbol.getDocumentationComment(); + + if (comments.length) { + definition.description = comments.map(comment => comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n")).join(""); + } + + // jsdocs are separate from comments + const jsdocs = symbol.getJsDocTags(); + jsdocs.forEach(doc => { + // if we have @TJS-... annotations, we have to parse them + const { name, text } = doc; + if (this.userValidationKeywords[name]) { + definition[name] = this.parseValue(text); + } else { + // special annotations + otherAnnotations[doc.name] = true; + } + }); +} + + +// excerpted from https://github.com/vega/ts-json-schema-generator +export interface Annotations { + [name: string]: any; +} +function getAnnotations(this: any, node: ts.Node): Annotations | undefined { + const symbol: ts.Symbol = (node as any).symbol; + if (!symbol) { + return undefined; + } + + const jsDocTags: ts.JSDocTagInfo[] = symbol.getJsDocTags(); + if (!jsDocTags || !jsDocTags.length) { + return undefined; + } + + const annotations: Annotations = jsDocTags.reduce((result: Annotations, jsDocTag: ts.JSDocTagInfo) => { + const value = this.parseJsDocTag(jsDocTag); + if (value !== undefined) { + result[jsDocTag.name] = value; + } + + return result; + }, {}); + return Object.keys(annotations).length ? annotations : undefined; +} + +// these examples are artificial and mostly nonsensical +function parseSpecificTags(node: ts.Node) { + if (node.kind === ts.SyntaxKind.Parameter) { + return ts.getJSDocParameterTags(node as ts.ParameterDeclaration); + } + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + const func = node as ts.FunctionDeclaration; + if (ts.hasJSDocParameterTags(func)) { + const flat: ts.JSDocTag[] = []; + for (const tags of func.parameters.map(ts.getJSDocParameterTags)) { + if (tags) flat.push(...tags); + } + return flat; + } + } +} + +function getReturnTypeFromJSDoc(node: ts.Node) { + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + return ts.getJSDocReturnType(node); + } + let type = ts.getJSDocType(node); + if (type && type.kind === ts.SyntaxKind.FunctionType) { + return (type as ts.FunctionTypeNode).type; + } +} + +function getAllTags(node: ts.Node) { + ts.getJSDocTags(node); +} + +function getSomeOtherTags(node: ts.Node) { + const tags: (ts.JSDocTag | undefined)[] = []; + tags.push(ts.getJSDocAugmentsOrExtendsTag(node)); + tags.push(ts.getJSDocClassTag(node)); + tags.push(ts.getJSDocReturnTag(node)); + const type = ts.getJSDocTypeTag(node); + if (type) { + tags.push(type); + } + tags.push(ts.getJSDocTemplateTag(node)); + return tags; +} diff --git a/tests/cases/fourslash/jsDocAugments.ts b/tests/cases/fourslash/jsDocAugments.ts index 24458c529f..cd2190e548 100644 --- a/tests/cases/fourslash/jsDocAugments.ts +++ b/tests/cases/fourslash/jsDocAugments.ts @@ -15,9 +15,8 @@ // @Filename: declarations.d.ts //// declare class Thing { -//// mine: T; +//// mine: T; //// } goTo.marker(); verify.quickInfoIs("(local var) x: string"); - diff --git a/tests/cases/fourslash/jsDocExtends.ts b/tests/cases/fourslash/jsDocExtends.ts new file mode 100644 index 0000000000..6bce556953 --- /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");