UNPKG

@microsoft/api-extractor

Version:

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

278 lines 10.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 }); const ts = require("typescript"); /** * Specifies various transformations that will be performed by Span.getModifiedText(). */ class SpanModification { constructor(span) { this.span = span; this.reset(); } /** * Allows the Span.prefix text to be changed. */ get prefix() { return this._prefix !== undefined ? this._prefix : this.span.prefix; } set prefix(value) { this._prefix = value; } /** * Allows the Span.suffix text to be changed. */ get suffix() { return this._suffix !== undefined ? this._suffix : this.span.suffix; } set suffix(value) { this._suffix = value; } /** * Reverts any modifications made to this object. */ reset() { this.omitChildren = false; this.omitSeparatorAfter = false; this._prefix = undefined; this._suffix = undefined; } /** * Effectively deletes the Span from the tree, by skipping its children, skipping its separator, * and setting its prefix/suffix to the empty string. */ skipAll() { this.prefix = ''; this.suffix = ''; this.omitChildren = true; this.omitSeparatorAfter = true; } } exports.SpanModification = SpanModification; /** * The Span class provides a simple way to rewrite TypeScript source files * based on simple syntax transformations, i.e. without having to process deeper aspects * of the underlying grammar. An example transformation might be deleting JSDoc comments * from a source file. * * @remarks * TypeScript's abstract syntax tree (AST) is represented using Node objects. * The Node text ignores its surrounding whitespace, and does not have an ordering guarantee. * For example, a JSDocComment node can be a child of a FunctionDeclaration node, even though * the actual comment precedes the function in the input stream. * * The Span class is a wrapper for a single Node, that provides access to every character * in the input stream, such that Span.getText() will exactly reproduce the corresponding * full Node.getText() output. * * A Span is comprised of these parts, which appear in sequential order: * - A prefix * - A collection of child spans * - A suffix * - A separator (e.g. whitespace between this span and the next item in the tree) * * These parts can be modified via Span.modification. The modification is applied by * calling Span.getModifiedText(). */ class Span { constructor(node) { this.node = node; this.startIndex = node.getStart(); this.endIndex = node.end; this._separatorStartIndex = 0; this._separatorEndIndex = 0; this.children = []; this.modification = new SpanModification(this); let previousChildSpan = undefined; for (const childNode of this.node.getChildren() || []) { const childSpan = new Span(childNode); childSpan._parent = this; childSpan._previousSibling = previousChildSpan; if (previousChildSpan) { previousChildSpan._nextSibling = childSpan; } this.children.push(childSpan); // Normalize the bounds so that a child is never outside its parent if (childSpan.startIndex < this.startIndex) { this.startIndex = childSpan.startIndex; } if (childSpan.endIndex > this.endIndex) { // This has never been observed empirically, but here's how we would handle it this.endIndex = childSpan.endIndex; throw new Error('Unexpected AST case'); } if (previousChildSpan) { if (previousChildSpan.endIndex < childSpan.startIndex) { // There is some leftover text after previous child -- assign it as the separator for // the preceding span. If the preceding span has no suffix, then assign it to the // deepest preceding span with no suffix. This heuristic simplifies the most // common transformations, and otherwise it can be fished out using getLastInnerSeparator(). let separatorRecipient = previousChildSpan; while (separatorRecipient.children.length > 0) { const lastChild = separatorRecipient.children[separatorRecipient.children.length - 1]; if (lastChild.endIndex !== separatorRecipient.endIndex) { // There is a suffix, so we cannot push the separator any further down, or else // it would get printed before this suffix. break; } separatorRecipient = lastChild; } separatorRecipient._separatorStartIndex = previousChildSpan.endIndex; separatorRecipient._separatorEndIndex = childSpan.startIndex; } } previousChildSpan = childSpan; } } get kind() { return this.node.kind; } /** * The parent Span, if any. * NOTE: This will be undefined for a root Span, even though the corresponding Node * may have a parent in the AST. */ get parent() { return this._parent; } /** * If the current object is this.parent.children[i], then previousSibling corresponds * to this.parent.children[i-1] if it exists. * NOTE: This will be undefined for a root Span, even though the corresponding Node * may have a previous sibling in the AST. */ get previousSibling() { return this._previousSibling; } /** * If the current object is this.parent.children[i], then previousSibling corresponds * to this.parent.children[i+1] if it exists. * NOTE: This will be undefined for a root Span, even though the corresponding Node * may have a previous sibling in the AST. */ get nextSibling() { return this._nextSibling; } /** * The text associated with the underlying Node, up to its first child. */ get prefix() { if (this.children.length) { // Everything up to the first child return this._getSubstring(this.startIndex, this.children[0].startIndex); } else { return this._getSubstring(this.startIndex, this.endIndex); } } /** * The text associated with the underlying Node, after its last child. * If there are no children, this is always an empty string. */ get suffix() { if (this.children.length) { // Everything after the last child return this._getSubstring(this.children[this.children.length - 1].endIndex, this.endIndex); } else { return ''; } } /** * Whitespace that appeared after this node, and before the "next" node in the tree. * Here we mean "next" according to an inorder traversal, not necessarily a sibling. */ get separator() { return this._getSubstring(this._separatorStartIndex, this._separatorEndIndex); } /** * Returns the separator of this Span, or else recursively calls getLastInnerSeparator() * on the last child. */ getLastInnerSeparator() { if (this.separator) { return this.separator; } if (this.children.length > 0) { return this.children[this.children.length - 1].getLastInnerSeparator(); } return ''; } /** * Recursively invokes the callback on this Span and all its children. The callback * can make changes to Span.modification for each node. */ forEach(callback) { callback(this); for (const child of this.children) { child.forEach(callback); } } /** * Returns the original unmodified text represented by this Span. */ getText() { let result = ''; result += this.prefix; for (const child of this.children) { result += child.getText(); } result += this.suffix; result += this.separator; return result; } /** * Returns the text represented by this Span, after applying all requested modifications. */ getModifiedText() { let result = ''; result += this.modification.prefix; if (!this.modification.omitChildren) { for (const child of this.children) { result += child.getModifiedText(); } } result += this.modification.suffix; if (!this.modification.omitSeparatorAfter) { result += this.separator; } return result; } /** * Returns a diagnostic dump of the tree, showing the prefix/suffix/separator for * each node. */ getDump(indent = '') { let result = indent + ts.SyntaxKind[this.node.kind] + ': '; if (this.prefix) { result += ' pre=[' + this._getTrimmed(this.prefix) + ']'; } if (this.suffix) { result += ' suf=[' + this._getTrimmed(this.suffix) + ']'; } if (this.separator) { result += ' sep=[' + this._getTrimmed(this.separator) + ']'; } result += '\n'; for (const child of this.children) { result += child.getDump(indent + ' '); } return result; } _getTrimmed(text) { const trimmed = text.replace(/[\r\n]/g, '\\n'); if (trimmed.length > 100) { return trimmed.substr(0, 97) + '...'; } return trimmed; } _getSubstring(startIndex, endIndex) { if (startIndex === endIndex) { return ''; } return this.node.getSourceFile().text.substring(startIndex, endIndex); } } exports.Span = Span; //# sourceMappingURL=Span.js.map