577 lines
23 KiB
JavaScript
577 lines
23 KiB
JavaScript
"use strict";
|
|
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
// See LICENSE in the project root for license information.
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Span = exports.SpanModification = exports.IndentDocCommentScope = void 0;
|
|
const ts = __importStar(require("typescript"));
|
|
const node_core_library_1 = require("@rushstack/node-core-library");
|
|
const IndentedWriter_1 = require("../generators/IndentedWriter");
|
|
var IndentDocCommentState;
|
|
(function (IndentDocCommentState) {
|
|
/**
|
|
* `indentDocComment` was not requested for this subtree.
|
|
*/
|
|
IndentDocCommentState[IndentDocCommentState["Inactive"] = 0] = "Inactive";
|
|
/**
|
|
* `indentDocComment` was requested and we are looking for the opening `/` `*`
|
|
*/
|
|
IndentDocCommentState[IndentDocCommentState["AwaitingOpenDelimiter"] = 1] = "AwaitingOpenDelimiter";
|
|
/**
|
|
* `indentDocComment` was requested and we are looking for the closing `*` `/`
|
|
*/
|
|
IndentDocCommentState[IndentDocCommentState["AwaitingCloseDelimiter"] = 2] = "AwaitingCloseDelimiter";
|
|
/**
|
|
* `indentDocComment` was requested and we have finished indenting the comment.
|
|
*/
|
|
IndentDocCommentState[IndentDocCommentState["Done"] = 3] = "Done";
|
|
})(IndentDocCommentState || (IndentDocCommentState = {}));
|
|
/**
|
|
* Choices for SpanModification.indentDocComment.
|
|
*/
|
|
var IndentDocCommentScope;
|
|
(function (IndentDocCommentScope) {
|
|
/**
|
|
* Do not detect and indent comments.
|
|
*/
|
|
IndentDocCommentScope[IndentDocCommentScope["None"] = 0] = "None";
|
|
/**
|
|
* Look for one doc comment in the {@link Span.prefix} text only.
|
|
*/
|
|
IndentDocCommentScope[IndentDocCommentScope["PrefixOnly"] = 1] = "PrefixOnly";
|
|
/**
|
|
* Look for one doc comment potentially distributed across the Span and its children.
|
|
*/
|
|
IndentDocCommentScope[IndentDocCommentScope["SpanAndChildren"] = 2] = "SpanAndChildren";
|
|
})(IndentDocCommentScope = exports.IndentDocCommentScope || (exports.IndentDocCommentScope = {}));
|
|
/**
|
|
* Specifies various transformations that will be performed by Span.getModifiedText().
|
|
*/
|
|
class SpanModification {
|
|
constructor(span) {
|
|
/**
|
|
* If true, all of the child spans will be omitted from the Span.getModifiedText() output.
|
|
* @remarks
|
|
* Also, the modify() operation will not recurse into these spans.
|
|
*/
|
|
this.omitChildren = false;
|
|
/**
|
|
* If true, then the Span.separator will be removed from the Span.getModifiedText() output.
|
|
*/
|
|
this.omitSeparatorAfter = false;
|
|
/**
|
|
* If true, then Span.getModifiedText() will sort the immediate children according to their Span.sortKey
|
|
* property. The separators will also be fixed up to ensure correct indentation. If the Span.sortKey is undefined
|
|
* for some items, those items will not be moved, i.e. their array indexes will be unchanged.
|
|
*/
|
|
this.sortChildren = false;
|
|
/**
|
|
* Optionally configures getModifiedText() to search for a "/*" doc comment and indent it.
|
|
* At most one comment is detected.
|
|
*
|
|
* @remarks
|
|
* The indentation can be applied to the `Span.modifier.prefix` only, or it can be applied to the
|
|
* full subtree of nodes (as needed for `ts.SyntaxKind.JSDocComment` trees). However the enabled
|
|
* scopes must not overlap.
|
|
*
|
|
* This feature is enabled selectively because (1) we do not want to accidentally match `/*` appearing
|
|
* in a string literal or other expression that is not a comment, and (2) parsing comments is relatively
|
|
* expensive.
|
|
*/
|
|
this.indentDocComment = IndentDocCommentScope.None;
|
|
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.sortChildren = false;
|
|
this.sortKey = undefined;
|
|
this._prefix = undefined;
|
|
this._suffix = undefined;
|
|
if (this._span.kind === ts.SyntaxKind.JSDocComment) {
|
|
this.indentDocComment = IndentDocCommentScope.SpanAndChildren;
|
|
}
|
|
}
|
|
/**
|
|
* 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.kind === ts.SyntaxKind.SourceFile ? node.getFullStart() : 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 node_core_library_1.InternalError('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 '';
|
|
}
|
|
/**
|
|
* Returns the first parent node with the specified SyntaxKind, or undefined if there is no match.
|
|
*/
|
|
findFirstParent(kindToMatch) {
|
|
let current = this;
|
|
while (current) {
|
|
if (current.kind === kindToMatch) {
|
|
return current;
|
|
}
|
|
current = current.parent;
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* 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() {
|
|
const writer = new IndentedWriter_1.IndentedWriter();
|
|
writer.trimLeadingSpaces = true;
|
|
this._writeModifiedText({
|
|
writer: writer,
|
|
separatorOverride: undefined,
|
|
indentDocCommentState: IndentDocCommentState.Inactive
|
|
});
|
|
return writer.getText();
|
|
}
|
|
writeModifiedText(output) {
|
|
this._writeModifiedText({
|
|
writer: output,
|
|
separatorOverride: undefined,
|
|
indentDocCommentState: IndentDocCommentState.Inactive
|
|
});
|
|
}
|
|
/**
|
|
* 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;
|
|
}
|
|
/**
|
|
* Returns a diagnostic dump of the tree, showing the SpanModification settings for each nodde.
|
|
*/
|
|
getModifiedDump(indent = '') {
|
|
let result = indent + ts.SyntaxKind[this.node.kind] + ': ';
|
|
if (this.prefix) {
|
|
result += ' pre=[' + this._getTrimmed(this.modification.prefix) + ']';
|
|
}
|
|
if (this.suffix) {
|
|
result += ' suf=[' + this._getTrimmed(this.modification.suffix) + ']';
|
|
}
|
|
if (this.separator) {
|
|
result += ' sep=[' + this._getTrimmed(this.separator) + ']';
|
|
}
|
|
if (this.modification.indentDocComment !== IndentDocCommentScope.None) {
|
|
result += ' indentDocComment=' + IndentDocCommentScope[this.modification.indentDocComment];
|
|
}
|
|
if (this.modification.omitChildren) {
|
|
result += ' omitChildren';
|
|
}
|
|
if (this.modification.omitSeparatorAfter) {
|
|
result += ' omitSeparatorAfter';
|
|
}
|
|
if (this.modification.sortChildren) {
|
|
result += ' sortChildren';
|
|
}
|
|
if (this.modification.sortKey !== undefined) {
|
|
result += ` sortKey="${this.modification.sortKey}"`;
|
|
}
|
|
result += '\n';
|
|
if (!this.modification.omitChildren) {
|
|
for (const child of this.children) {
|
|
result += child.getModifiedDump(indent + ' ');
|
|
}
|
|
}
|
|
else {
|
|
result += `${indent} (${this.children.length} children)\n`;
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Recursive implementation of `getModifiedText()` and `writeModifiedText()`.
|
|
*/
|
|
_writeModifiedText(options) {
|
|
// Apply indentation based on "{" and "}"
|
|
if (this.prefix === '{') {
|
|
options.writer.increaseIndent();
|
|
}
|
|
else if (this.prefix === '}') {
|
|
options.writer.decreaseIndent();
|
|
}
|
|
if (this.modification.indentDocComment !== IndentDocCommentScope.None) {
|
|
this._beginIndentDocComment(options);
|
|
}
|
|
this._write(this.modification.prefix, options);
|
|
if (this.modification.indentDocComment === IndentDocCommentScope.PrefixOnly) {
|
|
this._endIndentDocComment(options);
|
|
}
|
|
let sortedSubset;
|
|
if (!this.modification.omitChildren) {
|
|
if (this.modification.sortChildren) {
|
|
// We will only sort the items with a sortKey
|
|
const filtered = this.children.filter((x) => x.modification.sortKey !== undefined);
|
|
// Is there at least one of them?
|
|
if (filtered.length > 1) {
|
|
sortedSubset = filtered;
|
|
}
|
|
}
|
|
}
|
|
if (sortedSubset) {
|
|
// This is the complicated special case that sorts an arbitrary subset of the child nodes,
|
|
// preserving the surrounding nodes.
|
|
const sortedSubsetCount = sortedSubset.length;
|
|
// Remember the separator for the first and last ones
|
|
const firstSeparator = sortedSubset[0].getLastInnerSeparator();
|
|
const lastSeparator = sortedSubset[sortedSubsetCount - 1].getLastInnerSeparator();
|
|
node_core_library_1.Sort.sortBy(sortedSubset, (x) => x.modification.sortKey);
|
|
const childOptions = Object.assign({}, options);
|
|
let sortedSubsetIndex = 0;
|
|
for (let index = 0; index < this.children.length; ++index) {
|
|
let current;
|
|
// Is this an item that we sorted?
|
|
if (this.children[index].modification.sortKey === undefined) {
|
|
// No, take the next item from the original array
|
|
current = this.children[index];
|
|
childOptions.separatorOverride = undefined;
|
|
}
|
|
else {
|
|
// Yes, take the next item from the sortedSubset
|
|
current = sortedSubset[sortedSubsetIndex++];
|
|
if (sortedSubsetIndex < sortedSubsetCount) {
|
|
childOptions.separatorOverride = firstSeparator;
|
|
}
|
|
else {
|
|
childOptions.separatorOverride = lastSeparator;
|
|
}
|
|
}
|
|
current._writeModifiedText(childOptions);
|
|
}
|
|
}
|
|
else {
|
|
// This is the normal case that does not need to sort children
|
|
const childrenLength = this.children.length;
|
|
if (!this.modification.omitChildren) {
|
|
if (options.separatorOverride !== undefined) {
|
|
// Special case where the separatorOverride is passed down to the "last inner separator" span
|
|
for (let i = 0; i < childrenLength; ++i) {
|
|
const child = this.children[i];
|
|
if (
|
|
// Only the last child inherits the separatorOverride, because only it can contain
|
|
// the "last inner separator" span
|
|
i < childrenLength - 1 ||
|
|
// If this.separator is specified, then we will write separatorOverride below, so don't pass it along
|
|
this.separator) {
|
|
const childOptions = Object.assign({}, options);
|
|
childOptions.separatorOverride = undefined;
|
|
child._writeModifiedText(childOptions);
|
|
}
|
|
else {
|
|
child._writeModifiedText(options);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// The normal simple case
|
|
for (const child of this.children) {
|
|
child._writeModifiedText(options);
|
|
}
|
|
}
|
|
}
|
|
this._write(this.modification.suffix, options);
|
|
if (options.separatorOverride !== undefined) {
|
|
if (this.separator || childrenLength === 0) {
|
|
this._write(options.separatorOverride, options);
|
|
}
|
|
}
|
|
else {
|
|
if (!this.modification.omitSeparatorAfter) {
|
|
this._write(this.separator, options);
|
|
}
|
|
}
|
|
}
|
|
if (this.modification.indentDocComment === IndentDocCommentScope.SpanAndChildren) {
|
|
this._endIndentDocComment(options);
|
|
}
|
|
}
|
|
_beginIndentDocComment(options) {
|
|
if (options.indentDocCommentState !== IndentDocCommentState.Inactive) {
|
|
throw new node_core_library_1.InternalError('indentDocComment cannot be nested');
|
|
}
|
|
options.indentDocCommentState = IndentDocCommentState.AwaitingOpenDelimiter;
|
|
}
|
|
_endIndentDocComment(options) {
|
|
if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) {
|
|
throw new node_core_library_1.InternalError('missing "*/" delimiter for comment block');
|
|
}
|
|
options.indentDocCommentState = IndentDocCommentState.Inactive;
|
|
}
|
|
/**
|
|
* Writes one chunk of `text` to the `options.writer`, applying the `indentDocComment` rewriting.
|
|
*/
|
|
_write(text, options) {
|
|
let parsedText = text;
|
|
if (options.indentDocCommentState === IndentDocCommentState.AwaitingOpenDelimiter) {
|
|
let index = parsedText.indexOf('/*');
|
|
if (index >= 0) {
|
|
index += '/*'.length;
|
|
options.writer.write(parsedText.substring(0, index));
|
|
parsedText = parsedText.substring(index);
|
|
options.indentDocCommentState = IndentDocCommentState.AwaitingCloseDelimiter;
|
|
options.writer.increaseIndent(' ');
|
|
}
|
|
}
|
|
if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) {
|
|
let index = parsedText.indexOf('*/');
|
|
if (index >= 0) {
|
|
index += '*/'.length;
|
|
options.writer.write(parsedText.substring(0, index));
|
|
parsedText = parsedText.substring(index);
|
|
options.indentDocCommentState = IndentDocCommentState.Done;
|
|
options.writer.decreaseIndent();
|
|
}
|
|
}
|
|
options.writer.write(parsedText);
|
|
}
|
|
_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
|