UNPKG

23.8 kBJavaScriptView Raw
1"use strict";
2// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3// See LICENSE in the project root for license information.
4var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5 if (k2 === undefined) k2 = k;
6 var desc = Object.getOwnPropertyDescriptor(m, k);
7 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8 desc = { enumerable: true, get: function() { return m[k]; } };
9 }
10 Object.defineProperty(o, k2, desc);
11}) : (function(o, m, k, k2) {
12 if (k2 === undefined) k2 = k;
13 o[k2] = m[k];
14}));
15var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16 Object.defineProperty(o, "default", { enumerable: true, value: v });
17}) : function(o, v) {
18 o["default"] = v;
19});
20var __importStar = (this && this.__importStar) || function (mod) {
21 if (mod && mod.__esModule) return mod;
22 var result = {};
23 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24 __setModuleDefault(result, mod);
25 return result;
26};
27Object.defineProperty(exports, "__esModule", { value: true });
28exports.Span = exports.SpanModification = exports.IndentDocCommentScope = void 0;
29const ts = __importStar(require("typescript"));
30const node_core_library_1 = require("@rushstack/node-core-library");
31const IndentedWriter_1 = require("../generators/IndentedWriter");
32var IndentDocCommentState;
33(function (IndentDocCommentState) {
34 /**
35 * `indentDocComment` was not requested for this subtree.
36 */
37 IndentDocCommentState[IndentDocCommentState["Inactive"] = 0] = "Inactive";
38 /**
39 * `indentDocComment` was requested and we are looking for the opening `/` `*`
40 */
41 IndentDocCommentState[IndentDocCommentState["AwaitingOpenDelimiter"] = 1] = "AwaitingOpenDelimiter";
42 /**
43 * `indentDocComment` was requested and we are looking for the closing `*` `/`
44 */
45 IndentDocCommentState[IndentDocCommentState["AwaitingCloseDelimiter"] = 2] = "AwaitingCloseDelimiter";
46 /**
47 * `indentDocComment` was requested and we have finished indenting the comment.
48 */
49 IndentDocCommentState[IndentDocCommentState["Done"] = 3] = "Done";
50})(IndentDocCommentState || (IndentDocCommentState = {}));
51/**
52 * Choices for SpanModification.indentDocComment.
53 */
54var IndentDocCommentScope;
55(function (IndentDocCommentScope) {
56 /**
57 * Do not detect and indent comments.
58 */
59 IndentDocCommentScope[IndentDocCommentScope["None"] = 0] = "None";
60 /**
61 * Look for one doc comment in the {@link Span.prefix} text only.
62 */
63 IndentDocCommentScope[IndentDocCommentScope["PrefixOnly"] = 1] = "PrefixOnly";
64 /**
65 * Look for one doc comment potentially distributed across the Span and its children.
66 */
67 IndentDocCommentScope[IndentDocCommentScope["SpanAndChildren"] = 2] = "SpanAndChildren";
68})(IndentDocCommentScope = exports.IndentDocCommentScope || (exports.IndentDocCommentScope = {}));
69/**
70 * Specifies various transformations that will be performed by Span.getModifiedText().
71 */
72class SpanModification {
73 constructor(span) {
74 /**
75 * If true, all of the child spans will be omitted from the Span.getModifiedText() output.
76 * @remarks
77 * Also, the modify() operation will not recurse into these spans.
78 */
79 this.omitChildren = false;
80 /**
81 * If true, then the Span.separator will be removed from the Span.getModifiedText() output.
82 */
83 this.omitSeparatorAfter = false;
84 /**
85 * If true, then Span.getModifiedText() will sort the immediate children according to their Span.sortKey
86 * property. The separators will also be fixed up to ensure correct indentation. If the Span.sortKey is undefined
87 * for some items, those items will not be moved, i.e. their array indexes will be unchanged.
88 */
89 this.sortChildren = false;
90 /**
91 * Optionally configures getModifiedText() to search for a "/*" doc comment and indent it.
92 * At most one comment is detected.
93 *
94 * @remarks
95 * The indentation can be applied to the `Span.modifier.prefix` only, or it can be applied to the
96 * full subtree of nodes (as needed for `ts.SyntaxKind.JSDocComment` trees). However the enabled
97 * scopes must not overlap.
98 *
99 * This feature is enabled selectively because (1) we do not want to accidentally match `/*` appearing
100 * in a string literal or other expression that is not a comment, and (2) parsing comments is relatively
101 * expensive.
102 */
103 this.indentDocComment = IndentDocCommentScope.None;
104 this._span = span;
105 this.reset();
106 }
107 /**
108 * Allows the Span.prefix text to be changed.
109 */
110 get prefix() {
111 return this._prefix !== undefined ? this._prefix : this._span.prefix;
112 }
113 set prefix(value) {
114 this._prefix = value;
115 }
116 /**
117 * Allows the Span.suffix text to be changed.
118 */
119 get suffix() {
120 return this._suffix !== undefined ? this._suffix : this._span.suffix;
121 }
122 set suffix(value) {
123 this._suffix = value;
124 }
125 /**
126 * Reverts any modifications made to this object.
127 */
128 reset() {
129 this.omitChildren = false;
130 this.omitSeparatorAfter = false;
131 this.sortChildren = false;
132 this.sortKey = undefined;
133 this._prefix = undefined;
134 this._suffix = undefined;
135 if (this._span.kind === ts.SyntaxKind.JSDocComment) {
136 this.indentDocComment = IndentDocCommentScope.SpanAndChildren;
137 }
138 }
139 /**
140 * Effectively deletes the Span from the tree, by skipping its children, skipping its separator,
141 * and setting its prefix/suffix to the empty string.
142 */
143 skipAll() {
144 this.prefix = '';
145 this.suffix = '';
146 this.omitChildren = true;
147 this.omitSeparatorAfter = true;
148 }
149}
150exports.SpanModification = SpanModification;
151/**
152 * The Span class provides a simple way to rewrite TypeScript source files
153 * based on simple syntax transformations, i.e. without having to process deeper aspects
154 * of the underlying grammar. An example transformation might be deleting JSDoc comments
155 * from a source file.
156 *
157 * @remarks
158 * TypeScript's abstract syntax tree (AST) is represented using Node objects.
159 * The Node text ignores its surrounding whitespace, and does not have an ordering guarantee.
160 * For example, a JSDocComment node can be a child of a FunctionDeclaration node, even though
161 * the actual comment precedes the function in the input stream.
162 *
163 * The Span class is a wrapper for a single Node, that provides access to every character
164 * in the input stream, such that Span.getText() will exactly reproduce the corresponding
165 * full Node.getText() output.
166 *
167 * A Span is comprised of these parts, which appear in sequential order:
168 * - A prefix
169 * - A collection of child spans
170 * - A suffix
171 * - A separator (e.g. whitespace between this span and the next item in the tree)
172 *
173 * These parts can be modified via Span.modification. The modification is applied by
174 * calling Span.getModifiedText().
175 */
176class Span {
177 constructor(node) {
178 this.node = node;
179 this.startIndex = node.kind === ts.SyntaxKind.SourceFile ? node.getFullStart() : node.getStart();
180 this.endIndex = node.end;
181 this._separatorStartIndex = 0;
182 this._separatorEndIndex = 0;
183 this.children = [];
184 this.modification = new SpanModification(this);
185 let previousChildSpan = undefined;
186 for (const childNode of this.node.getChildren() || []) {
187 const childSpan = new Span(childNode);
188 childSpan._parent = this;
189 childSpan._previousSibling = previousChildSpan;
190 if (previousChildSpan) {
191 previousChildSpan._nextSibling = childSpan;
192 }
193 this.children.push(childSpan);
194 // Normalize the bounds so that a child is never outside its parent
195 if (childSpan.startIndex < this.startIndex) {
196 this.startIndex = childSpan.startIndex;
197 }
198 if (childSpan.endIndex > this.endIndex) {
199 // This has never been observed empirically, but here's how we would handle it
200 this.endIndex = childSpan.endIndex;
201 throw new node_core_library_1.InternalError('Unexpected AST case');
202 }
203 if (previousChildSpan) {
204 if (previousChildSpan.endIndex < childSpan.startIndex) {
205 // There is some leftover text after previous child -- assign it as the separator for
206 // the preceding span. If the preceding span has no suffix, then assign it to the
207 // deepest preceding span with no suffix. This heuristic simplifies the most
208 // common transformations, and otherwise it can be fished out using getLastInnerSeparator().
209 let separatorRecipient = previousChildSpan;
210 while (separatorRecipient.children.length > 0) {
211 const lastChild = separatorRecipient.children[separatorRecipient.children.length - 1];
212 if (lastChild.endIndex !== separatorRecipient.endIndex) {
213 // There is a suffix, so we cannot push the separator any further down, or else
214 // it would get printed before this suffix.
215 break;
216 }
217 separatorRecipient = lastChild;
218 }
219 separatorRecipient._separatorStartIndex = previousChildSpan.endIndex;
220 separatorRecipient._separatorEndIndex = childSpan.startIndex;
221 }
222 }
223 previousChildSpan = childSpan;
224 }
225 }
226 get kind() {
227 return this.node.kind;
228 }
229 /**
230 * The parent Span, if any.
231 * NOTE: This will be undefined for a root Span, even though the corresponding Node
232 * may have a parent in the AST.
233 */
234 get parent() {
235 return this._parent;
236 }
237 /**
238 * If the current object is this.parent.children[i], then previousSibling corresponds
239 * to this.parent.children[i-1] if it exists.
240 * NOTE: This will be undefined for a root Span, even though the corresponding Node
241 * may have a previous sibling in the AST.
242 */
243 get previousSibling() {
244 return this._previousSibling;
245 }
246 /**
247 * If the current object is this.parent.children[i], then previousSibling corresponds
248 * to this.parent.children[i+1] if it exists.
249 * NOTE: This will be undefined for a root Span, even though the corresponding Node
250 * may have a previous sibling in the AST.
251 */
252 get nextSibling() {
253 return this._nextSibling;
254 }
255 /**
256 * The text associated with the underlying Node, up to its first child.
257 */
258 get prefix() {
259 if (this.children.length) {
260 // Everything up to the first child
261 return this._getSubstring(this.startIndex, this.children[0].startIndex);
262 }
263 else {
264 return this._getSubstring(this.startIndex, this.endIndex);
265 }
266 }
267 /**
268 * The text associated with the underlying Node, after its last child.
269 * If there are no children, this is always an empty string.
270 */
271 get suffix() {
272 if (this.children.length) {
273 // Everything after the last child
274 return this._getSubstring(this.children[this.children.length - 1].endIndex, this.endIndex);
275 }
276 else {
277 return '';
278 }
279 }
280 /**
281 * Whitespace that appeared after this node, and before the "next" node in the tree.
282 * Here we mean "next" according to an inorder traversal, not necessarily a sibling.
283 */
284 get separator() {
285 return this._getSubstring(this._separatorStartIndex, this._separatorEndIndex);
286 }
287 /**
288 * Returns the separator of this Span, or else recursively calls getLastInnerSeparator()
289 * on the last child.
290 */
291 getLastInnerSeparator() {
292 if (this.separator) {
293 return this.separator;
294 }
295 if (this.children.length > 0) {
296 return this.children[this.children.length - 1].getLastInnerSeparator();
297 }
298 return '';
299 }
300 /**
301 * Returns the first parent node with the specified SyntaxKind, or undefined if there is no match.
302 */
303 findFirstParent(kindToMatch) {
304 let current = this;
305 while (current) {
306 if (current.kind === kindToMatch) {
307 return current;
308 }
309 current = current.parent;
310 }
311 return undefined;
312 }
313 /**
314 * Recursively invokes the callback on this Span and all its children. The callback
315 * can make changes to Span.modification for each node.
316 */
317 forEach(callback) {
318 callback(this);
319 for (const child of this.children) {
320 child.forEach(callback);
321 }
322 }
323 /**
324 * Returns the original unmodified text represented by this Span.
325 */
326 getText() {
327 let result = '';
328 result += this.prefix;
329 for (const child of this.children) {
330 result += child.getText();
331 }
332 result += this.suffix;
333 result += this.separator;
334 return result;
335 }
336 /**
337 * Returns the text represented by this Span, after applying all requested modifications.
338 */
339 getModifiedText() {
340 const writer = new IndentedWriter_1.IndentedWriter();
341 writer.trimLeadingSpaces = true;
342 this._writeModifiedText({
343 writer: writer,
344 separatorOverride: undefined,
345 indentDocCommentState: IndentDocCommentState.Inactive
346 });
347 return writer.getText();
348 }
349 writeModifiedText(output) {
350 this._writeModifiedText({
351 writer: output,
352 separatorOverride: undefined,
353 indentDocCommentState: IndentDocCommentState.Inactive
354 });
355 }
356 /**
357 * Returns a diagnostic dump of the tree, showing the prefix/suffix/separator for
358 * each node.
359 */
360 getDump(indent = '') {
361 let result = indent + ts.SyntaxKind[this.node.kind] + ': ';
362 if (this.prefix) {
363 result += ' pre=[' + this._getTrimmed(this.prefix) + ']';
364 }
365 if (this.suffix) {
366 result += ' suf=[' + this._getTrimmed(this.suffix) + ']';
367 }
368 if (this.separator) {
369 result += ' sep=[' + this._getTrimmed(this.separator) + ']';
370 }
371 result += '\n';
372 for (const child of this.children) {
373 result += child.getDump(indent + ' ');
374 }
375 return result;
376 }
377 /**
378 * Returns a diagnostic dump of the tree, showing the SpanModification settings for each nodde.
379 */
380 getModifiedDump(indent = '') {
381 let result = indent + ts.SyntaxKind[this.node.kind] + ': ';
382 if (this.prefix) {
383 result += ' pre=[' + this._getTrimmed(this.modification.prefix) + ']';
384 }
385 if (this.suffix) {
386 result += ' suf=[' + this._getTrimmed(this.modification.suffix) + ']';
387 }
388 if (this.separator) {
389 result += ' sep=[' + this._getTrimmed(this.separator) + ']';
390 }
391 if (this.modification.indentDocComment !== IndentDocCommentScope.None) {
392 result += ' indentDocComment=' + IndentDocCommentScope[this.modification.indentDocComment];
393 }
394 if (this.modification.omitChildren) {
395 result += ' omitChildren';
396 }
397 if (this.modification.omitSeparatorAfter) {
398 result += ' omitSeparatorAfter';
399 }
400 if (this.modification.sortChildren) {
401 result += ' sortChildren';
402 }
403 if (this.modification.sortKey !== undefined) {
404 result += ` sortKey="${this.modification.sortKey}"`;
405 }
406 result += '\n';
407 if (!this.modification.omitChildren) {
408 for (const child of this.children) {
409 result += child.getModifiedDump(indent + ' ');
410 }
411 }
412 else {
413 result += `${indent} (${this.children.length} children)\n`;
414 }
415 return result;
416 }
417 /**
418 * Recursive implementation of `getModifiedText()` and `writeModifiedText()`.
419 */
420 _writeModifiedText(options) {
421 // Apply indentation based on "{" and "}"
422 if (this.prefix === '{') {
423 options.writer.increaseIndent();
424 }
425 else if (this.prefix === '}') {
426 options.writer.decreaseIndent();
427 }
428 if (this.modification.indentDocComment !== IndentDocCommentScope.None) {
429 this._beginIndentDocComment(options);
430 }
431 this._write(this.modification.prefix, options);
432 if (this.modification.indentDocComment === IndentDocCommentScope.PrefixOnly) {
433 this._endIndentDocComment(options);
434 }
435 let sortedSubset;
436 if (!this.modification.omitChildren) {
437 if (this.modification.sortChildren) {
438 // We will only sort the items with a sortKey
439 const filtered = this.children.filter((x) => x.modification.sortKey !== undefined);
440 // Is there at least one of them?
441 if (filtered.length > 1) {
442 sortedSubset = filtered;
443 }
444 }
445 }
446 if (sortedSubset) {
447 // This is the complicated special case that sorts an arbitrary subset of the child nodes,
448 // preserving the surrounding nodes.
449 const sortedSubsetCount = sortedSubset.length;
450 // Remember the separator for the first and last ones
451 const firstSeparator = sortedSubset[0].getLastInnerSeparator();
452 const lastSeparator = sortedSubset[sortedSubsetCount - 1].getLastInnerSeparator();
453 node_core_library_1.Sort.sortBy(sortedSubset, (x) => x.modification.sortKey);
454 const childOptions = Object.assign({}, options);
455 let sortedSubsetIndex = 0;
456 for (let index = 0; index < this.children.length; ++index) {
457 let current;
458 // Is this an item that we sorted?
459 if (this.children[index].modification.sortKey === undefined) {
460 // No, take the next item from the original array
461 current = this.children[index];
462 childOptions.separatorOverride = undefined;
463 }
464 else {
465 // Yes, take the next item from the sortedSubset
466 current = sortedSubset[sortedSubsetIndex++];
467 if (sortedSubsetIndex < sortedSubsetCount) {
468 childOptions.separatorOverride = firstSeparator;
469 }
470 else {
471 childOptions.separatorOverride = lastSeparator;
472 }
473 }
474 current._writeModifiedText(childOptions);
475 }
476 }
477 else {
478 // This is the normal case that does not need to sort children
479 const childrenLength = this.children.length;
480 if (!this.modification.omitChildren) {
481 if (options.separatorOverride !== undefined) {
482 // Special case where the separatorOverride is passed down to the "last inner separator" span
483 for (let i = 0; i < childrenLength; ++i) {
484 const child = this.children[i];
485 if (
486 // Only the last child inherits the separatorOverride, because only it can contain
487 // the "last inner separator" span
488 i < childrenLength - 1 ||
489 // If this.separator is specified, then we will write separatorOverride below, so don't pass it along
490 this.separator) {
491 const childOptions = Object.assign({}, options);
492 childOptions.separatorOverride = undefined;
493 child._writeModifiedText(childOptions);
494 }
495 else {
496 child._writeModifiedText(options);
497 }
498 }
499 }
500 else {
501 // The normal simple case
502 for (const child of this.children) {
503 child._writeModifiedText(options);
504 }
505 }
506 }
507 this._write(this.modification.suffix, options);
508 if (options.separatorOverride !== undefined) {
509 if (this.separator || childrenLength === 0) {
510 this._write(options.separatorOverride, options);
511 }
512 }
513 else {
514 if (!this.modification.omitSeparatorAfter) {
515 this._write(this.separator, options);
516 }
517 }
518 }
519 if (this.modification.indentDocComment === IndentDocCommentScope.SpanAndChildren) {
520 this._endIndentDocComment(options);
521 }
522 }
523 _beginIndentDocComment(options) {
524 if (options.indentDocCommentState !== IndentDocCommentState.Inactive) {
525 throw new node_core_library_1.InternalError('indentDocComment cannot be nested');
526 }
527 options.indentDocCommentState = IndentDocCommentState.AwaitingOpenDelimiter;
528 }
529 _endIndentDocComment(options) {
530 if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) {
531 throw new node_core_library_1.InternalError('missing "*/" delimiter for comment block');
532 }
533 options.indentDocCommentState = IndentDocCommentState.Inactive;
534 }
535 /**
536 * Writes one chunk of `text` to the `options.writer`, applying the `indentDocComment` rewriting.
537 */
538 _write(text, options) {
539 let parsedText = text;
540 if (options.indentDocCommentState === IndentDocCommentState.AwaitingOpenDelimiter) {
541 let index = parsedText.indexOf('/*');
542 if (index >= 0) {
543 index += '/*'.length;
544 options.writer.write(parsedText.substring(0, index));
545 parsedText = parsedText.substring(index);
546 options.indentDocCommentState = IndentDocCommentState.AwaitingCloseDelimiter;
547 options.writer.increaseIndent(' ');
548 }
549 }
550 if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) {
551 let index = parsedText.indexOf('*/');
552 if (index >= 0) {
553 index += '*/'.length;
554 options.writer.write(parsedText.substring(0, index));
555 parsedText = parsedText.substring(index);
556 options.indentDocCommentState = IndentDocCommentState.Done;
557 options.writer.decreaseIndent();
558 }
559 }
560 options.writer.write(parsedText);
561 }
562 _getTrimmed(text) {
563 const trimmed = text.replace(/\r?\n/g, '\\n');
564 if (trimmed.length > 100) {
565 return trimmed.substr(0, 97) + '...';
566 }
567 return trimmed;
568 }
569 _getSubstring(startIndex, endIndex) {
570 if (startIndex === endIndex) {
571 return '';
572 }
573 return this.node.getSourceFile().text.substring(startIndex, endIndex);
574 }
575}
576exports.Span = Span;
577//# sourceMappingURL=Span.js.map
\No newline at end of file