Files
discord.js/packages/api-extractor/src/analyzer/Span.ts
Qjuh 5c0fad3b2d build: package api-extractor and -model (#9920)
* fix(ExceptText): don't display import("d..-types/v10"). in return type

* Squashed 'packages/api-extractor-model/' content from commit 39ecb196c

git-subtree-dir: packages/api-extractor-model
git-subtree-split: 39ecb196ca210bdf84ba6c9cadb1bb93571849d7

* Squashed 'packages/api-extractor/' content from commit 341ad6c51

git-subtree-dir: packages/api-extractor
git-subtree-split: 341ad6c51b01656d4f73b74ad4bdb3095f9262c4

* feat(api-extractor): add api-extractor and -model

* fix: package.json docs script

* fix(SourcLink): use <> instead of function syntax

* fix: make packages private

* fix: rest params showing in docs, added labels

* fix: missed two files

* fix: cpy-cli & pnpm-lock

* fix: increase icon size

* fix: icon size again
2023-11-07 21:53:36 +01:00

684 lines
20 KiB
TypeScript

/* eslint-disable promise/prefer-await-to-callbacks */
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError, Sort } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import { IndentedWriter } from '../generators/IndentedWriter.js';
interface IWriteModifiedTextOptions {
indentDocCommentState: IndentDocCommentState;
separatorOverride: string | undefined;
writer: IndentedWriter;
}
enum IndentDocCommentState {
/**
* `indentDocComment` was not requested for this subtree.
*/
Inactive = 0,
/**
* `indentDocComment` was requested and we are looking for the opening `/` `*`
*/
AwaitingOpenDelimiter = 1,
/**
* `indentDocComment` was requested and we are looking for the closing `*` `/`
*/
AwaitingCloseDelimiter = 2,
/**
* `indentDocComment` was requested and we have finished indenting the comment.
*/
Done = 3,
}
/**
* Choices for SpanModification.indentDocComment.
*/
export enum IndentDocCommentScope {
/**
* Do not detect and indent comments.
*/
None = 0,
/**
* Look for one doc comment in the {@link Span.prefix} text only.
*/
PrefixOnly = 1,
/**
* Look for one doc comment potentially distributed across the Span and its children.
*/
SpanAndChildren = 2,
}
/**
* Specifies various transformations that will be performed by Span.getModifiedText().
*/
export class SpanModification {
/**
* 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.
*/
public omitChildren: boolean = false;
/**
* If true, then the Span.separator will be removed from the Span.getModifiedText() output.
*/
public omitSeparatorAfter: boolean = 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.
*/
public sortChildren: boolean = false;
/**
* Used if the parent span has Span.sortChildren=true.
*/
public sortKey: string | undefined;
/**
* 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.
*/
public indentDocComment: IndentDocCommentScope = IndentDocCommentScope.None;
private readonly _span: Span;
private _prefix: string | undefined;
private _suffix: string | undefined;
public constructor(span: Span) {
this._span = span;
this.reset();
}
/**
* Allows the Span.prefix text to be changed.
*/
public get prefix(): string {
return this._prefix ?? this._span.prefix;
}
public set prefix(value: string) {
this._prefix = value;
}
/**
* Allows the Span.suffix text to be changed.
*/
public get suffix(): string {
return this._suffix ?? this._span.suffix;
}
public set suffix(value: string) {
this._suffix = value;
}
/**
* Reverts any modifications made to this object.
*/
public reset(): void {
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.
*/
public skipAll(): void {
this.prefix = '';
this.suffix = '';
this.omitChildren = true;
this.omitSeparatorAfter = true;
}
}
/**
* 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().
*/
export class Span {
public readonly node: ts.Node;
// To improve performance, substrings are not allocated until actually needed
public readonly startIndex: number;
public readonly endIndex: number;
public readonly children: Span[];
public readonly modification: SpanModification;
private readonly _parent: Span | undefined;
private readonly _previousSibling: Span | undefined;
private readonly _nextSibling: Span | undefined;
private readonly _separatorStartIndex: number;
private readonly _separatorEndIndex: number;
public constructor(node: ts.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: Span | undefined;
for (const childNode of this.node.getChildren() || []) {
const childSpan: Span = new Span(childNode);
// @ts-expect-error assigning private readonly properties on creation only
childSpan._parent = this;
// @ts-expect-error assigning private readonly properties on creation only
childSpan._previousSibling = previousChildSpan;
if (previousChildSpan) {
// @ts-expect-error assigning private readonly properties on creation only
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 InternalError('Unexpected AST case');
}
if (previousChildSpan && 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: Span = previousChildSpan;
while (separatorRecipient.children.length > 0) {
const lastChild: Span = 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;
}
// @ts-expect-error assigning private readonly properties on creation only
separatorRecipient._separatorStartIndex = previousChildSpan.endIndex;
// @ts-expect-error assigning private readonly properties on creation only
separatorRecipient._separatorEndIndex = childSpan.startIndex;
}
previousChildSpan = childSpan;
}
}
public get kind(): ts.SyntaxKind {
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.
*/
public get parent(): Span | undefined {
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.
*/
public get previousSibling(): Span | undefined {
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.
*/
public get nextSibling(): Span | undefined {
return this._nextSibling;
}
/**
* The text associated with the underlying Node, up to its first child.
*/
public get prefix(): string {
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.
*/
public get suffix(): string {
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.
*/
public get separator(): string {
return this._getSubstring(this._separatorStartIndex, this._separatorEndIndex);
}
/**
* Returns the separator of this Span, or else recursively calls getLastInnerSeparator()
* on the last child.
*/
public getLastInnerSeparator(): string {
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.
*/
public findFirstParent(kindToMatch: ts.SyntaxKind): Span | undefined {
let current: Span | undefined = 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.
*/
public forEach(callback: (span: Span) => void): void {
// eslint-disable-next-line n/callback-return, n/no-callback-literal
callback(this);
for (const child of this.children) {
// eslint-disable-next-line unicorn/no-array-for-each
child.forEach(callback);
}
}
/**
* Returns the original unmodified text represented by this Span.
*/
public getText(): string {
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.
*/
public getModifiedText(): string {
const writer: IndentedWriter = new IndentedWriter();
writer.trimLeadingSpaces = true;
this._writeModifiedText({
writer,
separatorOverride: undefined,
indentDocCommentState: IndentDocCommentState.Inactive,
});
return writer.getText();
}
public writeModifiedText(output: IndentedWriter): void {
this._writeModifiedText({
writer: output,
separatorOverride: undefined,
indentDocCommentState: IndentDocCommentState.Inactive,
});
}
/**
* Returns a diagnostic dump of the tree, showing the prefix/suffix/separator for
* each node.
*/
public getDump(indent: string = ''): string {
let result: string = 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.
*/
public getModifiedDump(indent: string = ''): string {
let result: string = 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) {
result += `${indent} (${this.children.length} children)\n`;
} else {
for (const child of this.children) {
result += child.getModifiedDump(indent + ' ');
}
}
return result;
}
/**
* Recursive implementation of `getModifiedText()` and `writeModifiedText()`.
*/
private _writeModifiedText(options: IWriteModifiedTextOptions): void {
// 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: Span[] | undefined;
if (!this.modification.omitChildren && this.modification.sortChildren) {
// We will only sort the items with a sortKey
const filtered: Span[] = 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: number = sortedSubset.length;
// Remember the separator for the first and last ones
const firstSeparator: string = sortedSubset[0]!.getLastInnerSeparator();
const lastSeparator: string = sortedSubset[sortedSubsetCount - 1]!.getLastInnerSeparator();
Sort.sortBy(sortedSubset, (x) => x.modification.sortKey);
const childOptions: IWriteModifiedTextOptions = { ...options };
let sortedSubsetIndex = 0;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let index = 0; index < this.children.length; ++index) {
let current: Span;
// 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: number = this.children.length;
if (!this.modification.omitChildren) {
if (options.separatorOverride === undefined) {
// The normal simple case
for (const child of this.children) {
child._writeModifiedText(options);
}
} else {
// Special case where the separatorOverride is passed down to the "last inner separator" span
for (let index = 0; index < childrenLength; ++index) {
const child: Span = this.children[index]!;
if (
// Only the last child inherits the separatorOverride, because only it can contain
// the "last inner separator" span
index < childrenLength - 1 ||
// If this.separator is specified, then we will write separatorOverride below, so don't pass it along
this.separator
) {
const childOptions: IWriteModifiedTextOptions = { ...options };
childOptions.separatorOverride = undefined;
child._writeModifiedText(childOptions);
} else {
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);
}
}
private _beginIndentDocComment(options: IWriteModifiedTextOptions): void {
if (options.indentDocCommentState !== IndentDocCommentState.Inactive) {
throw new InternalError('indentDocComment cannot be nested');
}
options.indentDocCommentState = IndentDocCommentState.AwaitingOpenDelimiter;
}
private _endIndentDocComment(options: IWriteModifiedTextOptions): void {
if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) {
throw new InternalError('missing "*/" delimiter for comment block');
}
options.indentDocCommentState = IndentDocCommentState.Inactive;
}
/**
* Writes one chunk of `text` to the `options.writer`, applying the `indentDocComment` rewriting.
*/
private _write(text: string, options: IWriteModifiedTextOptions): void {
let parsedText: string = text;
if (options.indentDocCommentState === IndentDocCommentState.AwaitingOpenDelimiter) {
let index: number = parsedText.indexOf('/*');
if (index >= 0) {
index += '/*'.length;
options.writer.write(parsedText.slice(0, Math.max(0, index)));
parsedText = parsedText.slice(Math.max(0, index));
options.indentDocCommentState = IndentDocCommentState.AwaitingCloseDelimiter;
options.writer.increaseIndent(' ');
}
}
if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) {
let index: number = parsedText.indexOf('*/');
if (index >= 0) {
index += '*/'.length;
options.writer.write(parsedText.slice(0, Math.max(0, index)));
parsedText = parsedText.slice(Math.max(0, index));
options.indentDocCommentState = IndentDocCommentState.Done;
options.writer.decreaseIndent();
}
}
options.writer.write(parsedText);
}
private _getTrimmed(text: string): string {
const trimmed: string = text.replaceAll(/\r?\n/g, '\\n');
if (trimmed.length > 100) {
return trimmed.slice(0, 97) + '...';
}
return trimmed;
}
private _getSubstring(startIndex: number, endIndex: number): string {
if (startIndex === endIndex) {
return '';
}
return this.node.getSourceFile().text.slice(startIndex, endIndex);
}
}