@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
278 lines • 10.5 kB
JavaScript
"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