diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b1042b5a14fb5..f3b40c702b639 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1494,15 +1494,6 @@ namespace ts { ((node as JSDocFunctionType).parameters[0].name as Identifier).escapedText === "new"; } - export function hasJSDocParameterTags(node: FunctionLikeDeclaration | SignatureDeclaration): boolean { - return !!getFirstJSDocTag(node, SyntaxKind.JSDocParameterTag); - } - - function getFirstJSDocTag(node: Node, kind: SyntaxKind): JSDocTag | undefined { - const tags = getJSDocTags(node); - return find(tags, doc => doc.kind === kind); - } - export function getAllJSDocs(node: Node): (JSDoc | JSDocTag)[] { if (isJSDocTypedefTag(node)) { return [node.parent]; @@ -1510,16 +1501,7 @@ namespace ts { return getJSDocCommentsAndTags(node); } - export function getJSDocTags(node: Node): ReadonlyArray | undefined { - let tags = (node as JSDocContainer).jsDocCache; - // If cache is 'null', that means we did the work of searching for JSDoc tags and came up with nothing. - if (tags === undefined) { - (node as JSDocContainer).jsDocCache = tags = flatMap(getJSDocCommentsAndTags(node), j => isJSDoc(j) ? j.tags : j); - } - return tags; - } - - function getJSDocCommentsAndTags(node: Node): (JSDoc | JSDocTag)[] { + export function getJSDocCommentsAndTags(node: Node): (JSDoc | JSDocTag)[] { let result: Array | undefined; getJSDocCommentsAndTagsWorker(node); return result || emptyArray; @@ -1578,15 +1560,6 @@ namespace ts { } } - export function getJSDocParameterTags(param: ParameterDeclaration): JSDocParameterTag[] | undefined { - if (param.name && isIdentifier(param.name)) { - const name = param.name.escapedText; - return getJSDocTags(param.parent).filter((tag): tag is JSDocParameterTag => isJSDocParameterTag(tag) && isIdentifier(tag.name) && tag.name.escapedText === name) as JSDocParameterTag[]; - } - // a binding pattern doesn't have a name, so it's not possible to match it a jsdoc parameter, which is identified by name - return undefined; - } - /** Does the opposite of `getJSDocParameterTags`: given a JSDoc parameter, finds the parameter corresponding to it. */ export function getParameterSymbolFromJSDoc(node: JSDocParameterTag): Symbol | undefined { if (node.symbol) { @@ -1612,39 +1585,6 @@ namespace ts { return find(typeParameters, p => p.name.escapedText === name); } - export function getJSDocType(node: Node): TypeNode { - let tag: JSDocTypeTag | JSDocParameterTag = getFirstJSDocTag(node, SyntaxKind.JSDocTypeTag) as JSDocTypeTag; - if (!tag && node.kind === SyntaxKind.Parameter) { - const paramTags = getJSDocParameterTags(node as ParameterDeclaration); - if (paramTags) { - tag = find(paramTags, tag => !!tag.typeExpression); - } - } - - return tag && tag.typeExpression && tag.typeExpression.type; - } - - export function getJSDocAugmentsTag(node: Node): JSDocAugmentsTag { - return getFirstJSDocTag(node, SyntaxKind.JSDocAugmentsTag) as JSDocAugmentsTag; - } - - export function getJSDocClassTag(node: Node): JSDocClassTag { - return getFirstJSDocTag(node, SyntaxKind.JSDocClassTag) as JSDocClassTag; - } - - export function getJSDocReturnTag(node: Node): JSDocReturnTag { - return getFirstJSDocTag(node, SyntaxKind.JSDocReturnTag) as JSDocReturnTag; - } - - export function getJSDocReturnType(node: Node): TypeNode { - const returnTag = getJSDocReturnTag(node); - return returnTag && returnTag.typeExpression && returnTag.typeExpression.type; - } - - export function getJSDocTemplateTag(node: Node): JSDocTemplateTag { - return getFirstJSDocTag(node, SyntaxKind.JSDocTemplateTag) as JSDocTemplateTag; - } - export function hasRestParameter(s: SignatureDeclaration): boolean { return isRestParameter(lastOrUndefined(s.parameters)); } @@ -4086,6 +4026,114 @@ namespace ts { return (declaration as NamedDeclaration).name; } } + + /** + * Gets the JSDoc parameter tags for the node if present. + * + * @remarks Returns any JSDoc param tag that matches the provided + * parameter, whether a param tag on a containing function + * expression, or a param tag on a variable declaration whose + * initializer is the containing function. The tags closest to the + * node are returned first, so in the previous example, the param + * tag on the containing function expression would be first. + * + * Does not return tags for binding patterns, because JSDoc matches + * parameters by name and binding patterns do not have a name. + */ + export function getJSDocParameterTags(param: ParameterDeclaration): ReadonlyArray | undefined { + if (param.name && isIdentifier(param.name)) { + const name = param.name.escapedText; + return getJSDocTags(param.parent).filter((tag): tag is JSDocParameterTag => isJSDocParameterTag(tag) && isIdentifier(tag.name) && tag.name.escapedText === name) as JSDocParameterTag[]; + } + // a binding pattern doesn't have a name, so it's not possible to match it a JSDoc parameter, which is identified by name + return undefined; + } + + /** + * Return true if the node has JSDoc parameter tags. + * + * @remarks Includes parameter tags that are not directly on the node, + * for example on a variable declaration whose initializer is a function expression. + */ + export function hasJSDocParameterTags(node: FunctionLikeDeclaration | SignatureDeclaration): boolean { + return !!getFirstJSDocTag(node, SyntaxKind.JSDocParameterTag); + } + + /** Gets the JSDoc augments tag for the node if present */ + export function getJSDocAugmentsTag(node: Node): JSDocAugmentsTag | undefined { + return getFirstJSDocTag(node, SyntaxKind.JSDocAugmentsTag) as JSDocAugmentsTag; + } + + /** Gets the JSDoc class tag for the node if present */ + export function getJSDocClassTag(node: Node): JSDocClassTag | undefined { + return getFirstJSDocTag(node, SyntaxKind.JSDocClassTag) as JSDocClassTag; + } + + /** Gets the JSDoc return tag for the node if present */ + export function getJSDocReturnTag(node: Node): JSDocReturnTag | undefined { + return getFirstJSDocTag(node, SyntaxKind.JSDocReturnTag) as JSDocReturnTag; + } + + /** Gets the JSDoc template tag for the node if present */ + export function getJSDocTemplateTag(node: Node): JSDocTemplateTag | undefined { + return getFirstJSDocTag(node, SyntaxKind.JSDocTemplateTag) as JSDocTemplateTag; + } + + /** Gets the JSDoc type tag for the node if present */ + export function getJSDocTypeTag(node: Node): JSDocTypeTag | undefined { + return getFirstJSDocTag(node, SyntaxKind.JSDocTypeTag) as JSDocTypeTag; + } + + /** + * Gets the type node for the node if provided via JSDoc. + * + * @remarks The search includes any JSDoc param tag that relates + * to the provided parameter, for example a type tag on the + * parameter itself, or a param tag on a containing function + * expression, or a param tag on a variable declaration whose + * initializer is the containing function. The tags closest to the + * node are examined first, so in the previous example, the type + * tag directly on the node would be returned. + */ + export function getJSDocType(node: Node): TypeNode | undefined { + let tag: JSDocTypeTag | JSDocParameterTag = getFirstJSDocTag(node, SyntaxKind.JSDocTypeTag) as JSDocTypeTag; + if (!tag && node.kind === SyntaxKind.Parameter) { + const paramTags = getJSDocParameterTags(node as ParameterDeclaration); + if (paramTags) { + tag = find(paramTags, tag => !!tag.typeExpression); + } + } + + return tag && tag.typeExpression && tag.typeExpression.type; + } + + /** + * Gets the return type node for the node if provided via JSDoc's return tag. + * + * @remarks `getJSDocReturnTag` just gets the whole JSDoc tag. This function + * gets the type from inside the braces. + */ + export function getJSDocReturnType(node: Node): TypeNode | undefined { + const returnTag = getJSDocReturnTag(node); + return returnTag && returnTag.typeExpression && returnTag.typeExpression.type; + } + + /** Get all JSDoc tags related to a node, including those on parent nodes. */ + export function getJSDocTags(node: Node): ReadonlyArray | undefined { + let tags = (node as JSDocContainer).jsDocCache; + // If cache is 'null', that means we did the work of searching for JSDoc tags and came up with nothing. + if (tags === undefined) { + (node as JSDocContainer).jsDocCache = tags = flatMap(getJSDocCommentsAndTags(node), j => isJSDoc(j) ? j.tags : j); + } + return tags; + } + + /** Get the first JSDoc tag of a specified kind, or undefined if not present. */ + function getFirstJSDocTag(node: Node, kind: SyntaxKind): JSDocTag | undefined { + const tags = getJSDocTags(node); + return find(tags, doc => doc.kind === kind); + } + } // Simple node tests of the form `node.kind === SyntaxKind.Foo`. diff --git a/tests/baselines/reference/APISample_jsdoc.js b/tests/baselines/reference/APISample_jsdoc.js new file mode 100644 index 0000000000000..c74e188f38b22 --- /dev/null +++ b/tests/baselines/reference/APISample_jsdoc.js @@ -0,0 +1,212 @@ +//// [APISample_jsdoc.ts] +/* + * 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; +} + + +//// [APISample_jsdoc.js] +"use strict"; +/* + * 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 + */ +exports.__esModule = true; +var ts = require("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(symbol, definition, otherAnnotations) { + var _this = this; + if (!symbol) { + return; + } + // the comments for a symbol + var comments = symbol.getDocumentationComment(); + if (comments.length) { + definition.description = comments.map(function (comment) { return comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n"); }).join(""); + } + // jsdocs are separate from comments + var jsdocs = symbol.getJsDocTags(); + jsdocs.forEach(function (doc) { + // if we have @TJS-... annotations, we have to parse them + var name = doc.name, text = doc.text; + if (_this.userValidationKeywords[name]) { + definition[name] = _this.parseValue(text); + } + else { + // special annotations + otherAnnotations[doc.name] = true; + } + }); +} +function getAnnotations(node) { + var _this = this; + var symbol = node.symbol; + if (!symbol) { + return undefined; + } + var jsDocTags = symbol.getJsDocTags(); + if (!jsDocTags || !jsDocTags.length) { + return undefined; + } + var annotations = jsDocTags.reduce(function (result, jsDocTag) { + var 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) { + if (node.kind === ts.SyntaxKind.Parameter) { + return ts.getJSDocParameterTags(node); + } + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + var func = node; + if (ts.hasJSDocParameterTags(func)) { + var flat = []; + for (var _i = 0, _a = func.parameters.map(ts.getJSDocParameterTags); _i < _a.length; _i++) { + var tags = _a[_i]; + if (tags) + flat.push.apply(flat, tags); + } + return flat; + } + } +} +function getReturnTypeFromJSDoc(node) { + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + return ts.getJSDocReturnType(node); + } + var type = ts.getJSDocType(node); + if (type && type.kind === ts.SyntaxKind.FunctionType) { + return type.type; + } +} +function getAllTags(node) { + ts.getJSDocTags(node); +} +function getSomeOtherTags(node) { + var tags = []; + tags.push(ts.getJSDocAugmentsTag(node)); + tags.push(ts.getJSDocClassTag(node)); + tags.push(ts.getJSDocReturnTag(node)); + var type = ts.getJSDocTypeTag(node); + if (type) { + tags.push(type); + } + tags.push(ts.getJSDocTemplateTag(node)); + return tags; +} diff --git a/tests/cases/compiler/APISample_jsdoc.ts b/tests/cases/compiler/APISample_jsdoc.ts new file mode 100644 index 0000000000000..70b814ffff487 --- /dev/null +++ b/tests/cases/compiler/APISample_jsdoc.ts @@ -0,0 +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; +}