UNPKG

@microsoft/api-extractor

Version:

Validate, document, and review the exported API for a TypeScript library

298 lines 11.5 kB
"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. Object.defineProperty(exports, "__esModule", { value: true }); /* tslint:disable:no-bitwise */ const ts = require("typescript"); const PrettyPrinter_1 = require("./PrettyPrinter"); class TypeScriptHelpers { /** * This traverses any type aliases to find the original place where an item was defined. * For example, suppose a class is defined as "export default class MyClass { }" * but exported from the package's index.ts like this: * * export { default as _MyClass } from './MyClass'; * * In this example, calling followAliases() on the _MyClass symbol will return the * original definition of MyClass, traversing any intermediary places where the * symbol was imported and re-exported. */ static followAliases(symbol, typeChecker) { let current = symbol; while (true) { if (!(current.flags & ts.SymbolFlags.Alias)) { break; } const currentAlias = typeChecker.getAliasedSymbol(current); if (!currentAlias || currentAlias === current) { break; } current = currentAlias; } return current; } static getImmediateAliasedSymbol(symbol, typeChecker) { return typeChecker.getImmediateAliasedSymbol(symbol); // tslint:disable-line:no-any } /** * Returns the Symbol for the provided Declaration. This is a workaround for a missing * feature of the TypeScript Compiler API. It is the only apparent way to reach * certain data structures, and seems to always work, but is not officially documented. * * @returns The associated Symbol. If there is no semantic information (e.g. if the * declaration is an extra semicolon somewhere), then "undefined" is returned. */ static tryGetSymbolForDeclaration(declaration) { /* tslint:disable:no-any */ const symbol = declaration.symbol; /* tslint:enable:no-any */ return symbol; } /** * Same semantics as tryGetSymbolForDeclaration(), but throws an exception if the symbol * cannot be found. */ static getSymbolForDeclaration(declaration) { const symbol = TypeScriptHelpers.tryGetSymbolForDeclaration(declaration); if (!symbol) { throw new Error(PrettyPrinter_1.PrettyPrinter.formatFileAndLineNumber(declaration) + ': ' + 'Unable to determine semantic information for this declaration'); } return symbol; } /** * Retrieves the comment ranges associated with the specified node. */ static getJSDocCommentRanges(node, text) { // Compiler internal: // https://github.com/Microsoft/TypeScript/blob/v2.4.2/src/compiler/utilities.ts#L616 // tslint:disable-next-line:no-any return ts.getJSDocCommentRanges.apply(this, arguments); } /** * Similar to calling string.split() with a RegExp, except that the delimiters * are included in the result. * * Example: _splitStringWithRegEx("ABCDaFG", /A/gi) -> [ "A", "BCD", "a", "FG" ] * Example: _splitStringWithRegEx("", /A/gi) -> [ ] * Example: _splitStringWithRegEx("", /A?/gi) -> [ "" ] */ static splitStringWithRegEx(text, regExp) { if (!regExp.global) { throw new Error('RegExp must have the /g flag'); } if (text === undefined) { return []; } const result = []; let index = 0; let match; do { match = regExp.exec(text); if (match) { if (match.index > index) { result.push(text.substring(index, match.index)); } const matchText = match[0]; if (!matchText) { // It might be interesting to support matching e.g. '\b', but regExp.exec() // doesn't seem to iterate properly in this situation. throw new Error('The regular expression must match a nonzero number of characters'); } result.push(matchText); index = regExp.lastIndex; } } while (match && regExp.global); if (index < text.length) { result.push(text.substr(index)); } return result; } /** * Extracts the body of a JSDoc comment and returns it. */ // Examples: // "/**\n * this is\n * a test\n */\n" --> "this is\na test\n" // "/** single line comment */" --> "single line comment" static extractJSDocContent(text, errorLogger) { // Remove any leading/trailing whitespace around the comment characters, then split on newlines const lines = text.trim().split(TypeScriptHelpers._newLineRegEx); if (lines.length === 0) { return ''; } let matched; // Remove "/**" from the first line matched = false; lines[0] = lines[0].replace(TypeScriptHelpers._jsdocStartRegEx, () => { matched = true; return ''; }); if (!matched) { errorLogger('The comment does not begin with a \"/**\" delimiter.'); return ''; } // Remove "*/" from the last line matched = false; lines[lines.length - 1] = lines[lines.length - 1].replace(TypeScriptHelpers._jsdocEndRegEx, () => { matched = true; return ''; }); if (!matched) { errorLogger('The comment does not end with a \"*/\" delimiter.'); return ''; } // Remove a leading "*" from all lines except the first one for (let i = 1; i < lines.length; ++i) { lines[i] = lines[i].replace(TypeScriptHelpers._jsdocIntermediateRegEx, ''); } // Remove trailing spaces from all lines for (let i = 0; i < lines.length; ++i) { lines[i] = lines[i].replace(TypeScriptHelpers._jsdocTrimRightRegEx, ''); } // If the first line is blank, then remove it if (lines[0] === '') { lines.shift(); } return lines.join('\n'); } /** * Returns a JSDoc comment containing the provided content. * * @remarks * This is the inverse of the extractJSDocContent() operation. */ // Examples: // "this is\na test\n" --> "/**\n * this is\n * a test\n */\n" // "single line comment" --> "/** single line comment */" static formatJSDocContent(content) { if (!content) { return ''; } // If the string contains "*/", then replace it with "*\/" const escapedContent = content.replace(TypeScriptHelpers._jsdocCommentTerminator, '*\\/'); const lines = escapedContent.split(TypeScriptHelpers._newLineRegEx); if (lines.length === 0) { return ''; } if (lines.length < 2) { return `/** ${escapedContent} */`; } else { // If there was a trailing newline, remove it if (lines[lines.length - 1] === '') { lines.pop(); } return '/**\n * ' + lines.join('\n * ') + '\n */'; } } /** * Returns an ancestor of "node", such that the ancestor, any intermediary nodes, * and the starting node match a list of expected kinds. Undefined is returned * if there aren't enough ancestors, or if the kinds are incorrect. * * For example, suppose child "C" has parents A --> B --> C. * * Calling _matchAncestor(C, [ExportSpecifier, NamedExports, ExportDeclaration]) * would return A only if A is of kind ExportSpecifier, B is of kind NamedExports, * and C is of kind ExportDeclaration. * * Calling _matchAncestor(C, [ExportDeclaration]) would return C. */ static matchAncestor(node, kindsToMatch) { // (slice(0) clones an array) const reversedParentKinds = kindsToMatch.slice(0).reverse(); let current = undefined; for (const parentKind of reversedParentKinds) { if (!current) { // The first time through, start with node current = node; } else { // Then walk the parents current = current.parent; } // If we ran out of items, or if the kind doesn't match, then fail if (!current || current.kind !== parentKind) { return undefined; } } // If we matched everything, then return the node that matched the last parentKinds item return current; } /** * Does a depth-first search of the children of the specified node. Returns the first child * with the specified kind, or undefined if there is no match. */ static findFirstChildNode(node, kindToMatch) { for (const child of node.getChildren()) { if (child.kind === kindToMatch) { return child; } const recursiveMatch = TypeScriptHelpers.findFirstChildNode(child, kindToMatch); if (recursiveMatch) { return recursiveMatch; } } return undefined; } /** * Returns the first parent node with the specified SyntaxKind, or undefined if there is no match. * @remarks * This search will NOT match the starting node. */ static findFirstParent(node, kindToMatch) { let current = node.parent; while (current) { if (current.kind === kindToMatch) { return current; } current = current.parent; } return undefined; } /** * Returns the highest parent node with the specified SyntaxKind, or undefined if there is no match. * @remarks * Whereas findFirstParent() returns the first match, findHighestParent() returns the last match. */ static findHighestParent(node, kindToMatch) { let current = node; let highest = undefined; while (true) { current = TypeScriptHelpers.findFirstParent(current, kindToMatch); if (!current) { break; } highest = current; } return highest; } } /** * Splits on CRLF and other newline sequences */ TypeScriptHelpers._newLineRegEx = /\r\n|\n\r|\r|\n/g; /** * Start sequence is '/**'. */ TypeScriptHelpers._jsdocStartRegEx = /^\s*\/\*\*+\s*/; /** * End sequence is '*\/'. */ TypeScriptHelpers._jsdocEndRegEx = /\s*\*+\/\s*$/; /** * Intermediate lines of JSDoc comment character. */ TypeScriptHelpers._jsdocIntermediateRegEx = /^\s*\*\s?/; /** * Trailing white space */ TypeScriptHelpers._jsdocTrimRightRegEx = /\s*$/; /** * Invalid comment sequence */ TypeScriptHelpers._jsdocCommentTerminator = /[*][/]/g; exports.TypeScriptHelpers = TypeScriptHelpers; //# sourceMappingURL=TypeScriptHelpers.js.map