• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    addToSeen, ArrowFunction, BindingElement, CharacterCodes, ClassElement, ClassExpression, ClassLikeDeclaration,
3    CommentRange, concatenate, ConstructorDeclaration, contains, createNodeFactory, createPrinter, createRange,
4    createSourceFile, createTextChange, createTextRangeFromSpan, createTextSpan, createTextSpanFromRange,
5    createTextWriter, Debug, DeclarationStatement, EmitHint, EmitTextWriter, endsWith, Expression, factory,
6    FileTextChanges, filter, find, findChildOfKind, findLastIndex, findNextToken, findPrecedingToken, first,
7    firstOrUndefined, flatMap, flatMapToMutable, formatting, FunctionDeclaration, FunctionExpression, getAncestor,
8    getFirstNonSpaceCharacterPosition, getFormatCodeSettingsForWriting, getJSDocCommentRanges, getLeadingCommentRanges,
9    getLineAndCharacterOfPosition, getLineOfLocalPosition, getLineStartPositionForPosition, getNewLineKind,
10    getNewLineOrDefaultFromHost, getNodeId, getPrecedingNonSpaceCharacterPosition, getScriptKindFromFileName,
11    getShebang, getStartPositionOfLine, getTokenAtPosition, getTouchingToken, getTrailingCommentRanges, group, HasJSDoc,
12    hasJSDocNodes, ImportClause, ImportSpecifier, indexOfNode, InterfaceDeclaration, intersperse, isAnyImportSyntax,
13    isArray, isArrowFunction, isCallExpression, isClassElement, isClassOrTypeElement, isExpressionStatement,
14    isFunctionDeclaration, isFunctionExpression, isFunctionLike, isIdentifier, isImportClause, isImportDeclaration,
15    isImportSpecifier, isInComment, isInJSXText, isInString, isInTemplateString, isInterfaceDeclaration,
16    isJsonSourceFile, isLineBreak, isNamedImports, isObjectLiteralExpression, isParameter, isPinnedComment,
17    isPrologueDirective, isPropertyDeclaration, isPropertySignature, isRecognizedTripleSlashComment, isStatement,
18    isStatementButNotDeclaration, isString, isStringLiteral, isSuperCall, isVariableDeclaration, isWhiteSpaceLike,
19    isWhiteSpaceSingleLine, JSDoc, JSDocComment, JSDocParameterTag, JSDocReturnTag, JSDocTag, JSDocTypeTag,
20    LanguageServiceHost, last, lastOrUndefined, length, Map, mapDefined, MethodSignature, Modifier, NamedImportBindings,
21    NamedImports, NamespaceImport, Node, NodeArray, NodeFactoryFlags, nodeIsSynthesized, nullTransformationContext,
22    ObjectLiteralElementLike, ObjectLiteralExpression, ParameterDeclaration, positionsAreOnSameLine, PrintHandlers,
23    PrologueDirective, PropertyAssignment, PropertyDeclaration, PropertySignature, rangeContainsPosition,
24    rangeContainsRangeExclusive, rangeOfNode, rangeOfTypeParameters, rangeStartPositionsAreOnSameLine, removeSuffix,
25    ScriptKind, ScriptTarget, Set, setTextRangePosEnd, SignatureDeclaration, singleOrUndefined, skipTrivia, SourceFile,
26    SourceFileLike, stableSort, Statement, stringContainsAt, Symbol, SyntaxKind, TextChange, TextRange, textSpanEnd,
27    Token, tokenToString, TransformationContext, TypeLiteralNode, TypeNode, TypeParameterDeclaration, UserPreferences,
28    VariableDeclaration, VariableStatement, visitEachChild, visitNodes, Visitor,
29} from "./_namespaces/ts";
30
31/**
32 * Currently for simplicity we store recovered positions on the node itself.
33 * It can be changed to side-table later if we decide that current design is too invasive.
34 */
35function getPos(n: TextRange): number {
36    const result = (n as any).__pos;
37    Debug.assert(typeof result === "number");
38    return result;
39}
40
41function setPos(n: TextRange, pos: number): void {
42    Debug.assert(typeof pos === "number");
43    (n as any).__pos = pos;
44}
45
46function getEnd(n: TextRange): number {
47    const result = (n as any).__end;
48    Debug.assert(typeof result === "number");
49    return result;
50}
51
52function setEnd(n: TextRange, end: number): void {
53    Debug.assert(typeof end === "number");
54    (n as any).__end = end;
55}
56
57/** @internal */
58export interface ConfigurableStart {
59    leadingTriviaOption?: LeadingTriviaOption;
60}
61/** @internal */
62export interface ConfigurableEnd {
63    trailingTriviaOption?: TrailingTriviaOption;
64}
65
66/** @internal */
67export enum LeadingTriviaOption {
68    /** Exclude all leading trivia (use getStart()) */
69    Exclude,
70    /** Include leading trivia and,
71     * if there are no line breaks between the node and the previous token,
72     * include all trivia between the node and the previous token
73     */
74    IncludeAll,
75    /**
76     * Include attached JSDoc comments
77     */
78    JSDoc,
79    /**
80     * Only delete trivia on the same line as getStart().
81     * Used to avoid deleting leading comments
82     */
83    StartLine,
84}
85
86/** @internal */
87export enum TrailingTriviaOption {
88    /** Exclude all trailing trivia (use getEnd()) */
89    Exclude,
90    /** Doesn't include whitespace, but does strip comments */
91    ExcludeWhitespace,
92    /** Include trailing trivia */
93    Include,
94}
95
96function skipWhitespacesAndLineBreaks(text: string, start: number) {
97    return skipTrivia(text, start, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
98}
99
100function hasCommentsBeforeLineBreak(text: string, start: number) {
101    let i = start;
102    while (i < text.length) {
103        const ch = text.charCodeAt(i);
104        if (isWhiteSpaceSingleLine(ch)) {
105            i++;
106            continue;
107        }
108        return ch === CharacterCodes.slash;
109    }
110    return false;
111}
112
113/**
114 * Usually node.pos points to a position immediately after the previous token.
115 * If this position is used as a beginning of the span to remove - it might lead to removing the trailing trivia of the previous node, i.e:
116 * const x; // this is x
117 *        ^ - pos for the next variable declaration will point here
118 * const y; // this is y
119 *        ^ - end for previous variable declaration
120 * Usually leading trivia of the variable declaration 'y' should not include trailing trivia (whitespace, comment 'this is x' and newline) from the preceding
121 * variable declaration and trailing trivia for 'y' should include (whitespace, comment 'this is y', newline).
122 * By default when removing nodes we adjust start and end positions to respect specification of the trivia above.
123 * If pos\end should be interpreted literally (that is, withouth including leading and trailing trivia), `leadingTriviaOption` should be set to `LeadingTriviaOption.Exclude`
124 * and `trailingTriviaOption` to `TrailingTriviaOption.Exclude`.
125 *
126 * @internal
127 */
128export interface ConfigurableStartEnd extends ConfigurableStart, ConfigurableEnd {}
129
130const useNonAdjustedPositions: ConfigurableStartEnd = {
131    leadingTriviaOption: LeadingTriviaOption.Exclude,
132    trailingTriviaOption: TrailingTriviaOption.Exclude,
133};
134
135/** @internal */
136export interface InsertNodeOptions {
137    /**
138     * Text to be inserted before the new node
139     */
140    prefix?: string;
141    /**
142     * Text to be inserted after the new node
143     */
144    suffix?: string;
145    /**
146     * Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node
147     */
148    indentation?: number;
149    /**
150     * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind
151     */
152    delta?: number;
153}
154
155/** @internal */
156export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions {
157    readonly joiner?: string;
158}
159
160enum ChangeKind {
161    Remove,
162    ReplaceWithSingleNode,
163    ReplaceWithMultipleNodes,
164    Text,
165}
166
167type Change = ReplaceWithSingleNode | ReplaceWithMultipleNodes | RemoveNode | ChangeText;
168
169interface BaseChange {
170    readonly sourceFile: SourceFile;
171    readonly range: TextRange;
172}
173
174/** @internal */
175export interface ChangeNodeOptions extends ConfigurableStartEnd, InsertNodeOptions {}
176interface ReplaceWithSingleNode extends BaseChange {
177    readonly kind: ChangeKind.ReplaceWithSingleNode;
178    readonly node: Node;
179    readonly options?: InsertNodeOptions;
180}
181
182interface RemoveNode extends BaseChange {
183    readonly kind: ChangeKind.Remove;
184    readonly node?: never;
185    readonly options?: never;
186}
187
188interface ReplaceWithMultipleNodes extends BaseChange {
189    readonly kind: ChangeKind.ReplaceWithMultipleNodes;
190    readonly nodes: readonly Node[];
191    readonly options?: ReplaceWithMultipleNodesOptions;
192}
193
194interface ChangeText extends BaseChange {
195    readonly kind: ChangeKind.Text;
196    readonly text: string;
197}
198
199function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange {
200    return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) };
201}
202
203function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd, hasTrailingComment = false) {
204    const { leadingTriviaOption } = options;
205    if (leadingTriviaOption === LeadingTriviaOption.Exclude) {
206        return node.getStart(sourceFile);
207    }
208    if (leadingTriviaOption === LeadingTriviaOption.StartLine) {
209        const startPos = node.getStart(sourceFile);
210        const pos = getLineStartPositionForPosition(startPos, sourceFile);
211        return rangeContainsPosition(node, pos) ? pos : startPos;
212    }
213    if (leadingTriviaOption === LeadingTriviaOption.JSDoc) {
214        const JSDocComments = getJSDocCommentRanges(node, sourceFile.text);
215        if (JSDocComments?.length) {
216            return getLineStartPositionForPosition(JSDocComments[0].pos, sourceFile);
217        }
218    }
219    const fullStart = node.getFullStart();
220    const start = node.getStart(sourceFile);
221    if (fullStart === start) {
222        return start;
223    }
224    const fullStartLine = getLineStartPositionForPosition(fullStart, sourceFile);
225    const startLine = getLineStartPositionForPosition(start, sourceFile);
226    if (startLine === fullStartLine) {
227        // full start and start of the node are on the same line
228        //   a,     b;
229        //    ^     ^
230        //    |   start
231        // fullstart
232        // when b is replaced - we usually want to keep the leading trvia
233        // when b is deleted - we delete it
234        return leadingTriviaOption === LeadingTriviaOption.IncludeAll ? fullStart : start;
235    }
236
237    // if node has a trailing comments, use comment end position as the text has already been included.
238    if (hasTrailingComment) {
239        // Check first for leading comments as if the node is the first import, we want to exclude the trivia;
240        // otherwise we get the trailing comments.
241        const comment = getLeadingCommentRanges(sourceFile.text, fullStart)?.[0] || getTrailingCommentRanges(sourceFile.text, fullStart)?.[0];
242        if (comment) {
243            return skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
244        }
245    }
246
247    // get start position of the line following the line that contains fullstart position
248    // (but only if the fullstart isn't the very beginning of the file)
249    const nextLineStart = fullStart > 0 ? 1 : 0;
250    let adjustedStartPosition = getStartPositionOfLine(getLineOfLocalPosition(sourceFile, fullStartLine) + nextLineStart, sourceFile);
251    // skip whitespaces/newlines
252    adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition);
253    return getStartPositionOfLine(getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile);
254}
255
256/** Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; */
257function getEndPositionOfMultilineTrailingComment(sourceFile: SourceFile, node: Node, options: ConfigurableEnd): number | undefined {
258    const { end } = node;
259    const { trailingTriviaOption } = options;
260    if (trailingTriviaOption === TrailingTriviaOption.Include) {
261        // If the trailing comment is a multiline comment that extends to the next lines,
262        // return the end of the comment and track it for the next nodes to adjust.
263        const comments = getTrailingCommentRanges(sourceFile.text, end);
264        if (comments) {
265            const nodeEndLine = getLineOfLocalPosition(sourceFile, node.end);
266            for (const comment of comments) {
267                // Single line can break the loop as trivia will only be this line.
268                // Comments on subsequest lines are also ignored.
269                if (comment.kind === SyntaxKind.SingleLineCommentTrivia || getLineOfLocalPosition(sourceFile, comment.pos) > nodeEndLine) {
270                    break;
271                }
272
273                // Get the end line of the comment and compare against the end line of the node.
274                // If the comment end line position and the multiline comment extends to multiple lines,
275                // then is safe to return the end position.
276                const commentEndLine = getLineOfLocalPosition(sourceFile, comment.end);
277                if (commentEndLine > nodeEndLine) {
278                    return skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
279                }
280            }
281        }
282    }
283
284    return undefined;
285}
286
287function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd): number {
288    const { end } = node;
289    const { trailingTriviaOption } = options;
290    if (trailingTriviaOption === TrailingTriviaOption.Exclude) {
291        return end;
292    }
293    if (trailingTriviaOption === TrailingTriviaOption.ExcludeWhitespace) {
294        const comments = concatenate(getTrailingCommentRanges(sourceFile.text, end), getLeadingCommentRanges(sourceFile.text, end));
295        const realEnd = comments?.[comments.length - 1]?.end;
296        if (realEnd) {
297            return realEnd;
298        }
299        return end;
300    }
301
302    const multilineEndPosition = getEndPositionOfMultilineTrailingComment(sourceFile, node, options);
303    if (multilineEndPosition) {
304        return multilineEndPosition;
305    }
306
307    const newEnd = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true);
308
309    return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || isLineBreak(sourceFile.text.charCodeAt(newEnd - 1)))
310        ? newEnd
311        : end;
312}
313
314/**
315 * Checks if 'candidate' argument is a legal separator in the list that contains 'node' as an element
316 */
317function isSeparator(node: Node, candidate: Node | undefined): candidate is Token<SyntaxKind.CommaToken | SyntaxKind.SemicolonToken> {
318    return !!candidate && !!node.parent && (candidate.kind === SyntaxKind.CommaToken || (candidate.kind === SyntaxKind.SemicolonToken && node.parent.kind === SyntaxKind.ObjectLiteralExpression));
319}
320
321/** @internal */
322export interface TextChangesContext {
323    host: LanguageServiceHost;
324    formatContext: formatting.FormatContext;
325    preferences: UserPreferences;
326}
327
328/** @internal */
329export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature;
330
331/** @internal */
332export type ThisTypeAnnotatable = FunctionDeclaration | FunctionExpression;
333
334/** @internal */
335export function isThisTypeAnnotatable(containingFunction: SignatureDeclaration): containingFunction is ThisTypeAnnotatable {
336    return isFunctionExpression(containingFunction) || isFunctionDeclaration(containingFunction);
337}
338
339/** @internal */
340export class ChangeTracker {
341    private readonly changes: Change[] = [];
342    private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = [];
343    private readonly classesWithNodesInsertedAtStart = new Map<number, { readonly node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
344    private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray<TypeParameterDeclaration> }[] = [];
345
346    public static fromContext(context: TextChangesContext): ChangeTracker {
347        return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext);
348    }
349
350    public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[] {
351        const tracker = ChangeTracker.fromContext(context);
352        cb(tracker);
353        return tracker.getChanges();
354    }
355
356    /** Public for tests only. Other callers should use `ChangeTracker.with`. */
357    constructor(private readonly newLineCharacter: string, private readonly formatContext: formatting.FormatContext) {}
358
359    public pushRaw(sourceFile: SourceFile, change: FileTextChanges) {
360        Debug.assertEqual(sourceFile.fileName, change.fileName);
361        for (const c of change.textChanges) {
362            this.changes.push({
363                kind: ChangeKind.Text,
364                sourceFile,
365                text: c.newText,
366                range: createTextRangeFromSpan(c.span),
367            });
368        }
369    }
370
371    public deleteRange(sourceFile: SourceFile, range: TextRange): void {
372        this.changes.push({ kind: ChangeKind.Remove, sourceFile, range });
373    }
374
375    delete(sourceFile: SourceFile, node: Node | NodeArray<TypeParameterDeclaration>): void {
376        this.deletedNodes.push({ sourceFile, node });
377    }
378
379    /** Stop! Consider using `delete` instead, which has logic for deleting nodes from delimited lists. */
380    public deleteNode(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
381        this.deleteRange(sourceFile, getAdjustedRange(sourceFile, node, node, options));
382    }
383
384    public deleteNodes(sourceFile: SourceFile, nodes: readonly Node[], options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }, hasTrailingComment: boolean): void {
385        // When deleting multiple nodes we need to track if the end position is including multiline trailing comments.
386        for (const node of nodes) {
387            const pos = getAdjustedStartPosition(sourceFile, node, options, hasTrailingComment);
388            const end = getAdjustedEndPosition(sourceFile, node, options);
389
390            this.deleteRange(sourceFile, { pos, end });
391
392            hasTrailingComment = !!getEndPositionOfMultilineTrailingComment(sourceFile, node, options);
393        }
394    }
395
396    public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void {
397        this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) });
398    }
399
400    public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
401        const startPosition = getAdjustedStartPosition(sourceFile, startNode, options);
402        const endPosition = getAdjustedEndPosition(sourceFile, endNode, options);
403        this.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
404    }
405
406    public deleteNodeRangeExcludingEnd(sourceFile: SourceFile, startNode: Node, afterEndNode: Node | undefined, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
407        const startPosition = getAdjustedStartPosition(sourceFile, startNode, options);
408        const endPosition = afterEndNode === undefined ? sourceFile.text.length : getAdjustedStartPosition(sourceFile, afterEndNode, options);
409        this.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
410    }
411
412    public replaceRange(sourceFile: SourceFile, range: TextRange, newNode: Node, options: InsertNodeOptions = {}): void {
413        this.changes.push({ kind: ChangeKind.ReplaceWithSingleNode, sourceFile, range, options, node: newNode });
414    }
415
416    public replaceNode(sourceFile: SourceFile, oldNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void {
417        this.replaceRange(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNode, options);
418    }
419
420    public replaceNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void {
421        this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options);
422    }
423
424    private replaceRangeWithNodes(sourceFile: SourceFile, range: TextRange, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}): void {
425        this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes });
426    }
427
428    public replaceNodeWithNodes(sourceFile: SourceFile, oldNode: Node, newNodes: readonly Node[], options: ChangeNodeOptions = useNonAdjustedPositions): void {
429        this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options);
430    }
431
432    public replaceNodeWithText(sourceFile: SourceFile, oldNode: Node, text: string): void {
433        this.replaceRangeWithText(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, useNonAdjustedPositions), text);
434    }
435
436    public replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions): void {
437        this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options);
438    }
439
440    public nodeHasTrailingComment(sourceFile: SourceFile, oldNode: Node, configurableEnd: ConfigurableEnd = useNonAdjustedPositions): boolean {
441        return !!getEndPositionOfMultilineTrailingComment(sourceFile, oldNode, configurableEnd);
442    }
443
444    private nextCommaToken(sourceFile: SourceFile, node: Node): Node | undefined {
445        const next = findNextToken(node, node.parent, sourceFile);
446        return next && next.kind === SyntaxKind.CommaToken ? next : undefined;
447    }
448
449    public replacePropertyAssignment(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): void {
450        const suffix = this.nextCommaToken(sourceFile, oldNode) ? "" : ("," + this.newLineCharacter);
451        this.replaceNode(sourceFile, oldNode, newNode, { suffix });
452    }
453
454    public insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}): void {
455        this.replaceRange(sourceFile, createRange(pos), newNode, options);
456    }
457
458    private insertNodesAt(sourceFile: SourceFile, pos: number, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions = {}): void {
459        this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options);
460    }
461
462    public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void {
463        this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween);
464    }
465
466    public insertNodesAtTopOfFile(sourceFile: SourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void {
467        this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween);
468    }
469
470    private insertAtTopOfFile(sourceFile: SourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void {
471        const pos = getInsertionPositionAtSourceFileTop(sourceFile);
472        const options = {
473            prefix: pos === 0 ? undefined : this.newLineCharacter,
474            suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""),
475        };
476        if (isArray(insert)) {
477            this.insertNodesAt(sourceFile, pos, insert, options);
478        }
479        else {
480            this.insertNodeAt(sourceFile, pos, insert, options);
481        }
482    }
483
484    public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray<ParameterDeclaration>, newParam: ParameterDeclaration): void {
485        const p0 = firstOrUndefined(parameters);
486        if (p0) {
487            this.insertNodeBefore(sourceFile, p0, newParam);
488        }
489        else {
490            this.insertNodeAt(sourceFile, parameters.pos, newParam);
491        }
492    }
493
494    public insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, blankLineBetween = false, options: ConfigurableStartEnd = {}): void {
495        this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, options), newNode, this.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween));
496    }
497
498    public insertModifierAt(sourceFile: SourceFile, pos: number, modifier: SyntaxKind, options: InsertNodeOptions = {}): void {
499        this.insertNodeAt(sourceFile, pos, factory.createToken(modifier), options);
500    }
501
502    public insertModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void {
503        return this.insertModifierAt(sourceFile, before.getStart(sourceFile), modifier, { suffix: " " });
504    }
505
506    public insertCommentBeforeLine(sourceFile: SourceFile, lineNumber: number, position: number, commentText: string): void {
507        const lineStartPosition = getStartPositionOfLine(lineNumber, sourceFile);
508        const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition);
509        // First try to see if we can put the comment on the previous line.
510        // We need to make sure that we are not in the middle of a string literal or a comment.
511        // If so, we do not want to separate the node from its comment if we can.
512        // Otherwise, add an extra new line immediately before the error span.
513        const insertAtLineStart = isValidLocationToAddComment(sourceFile, startPosition);
514        const token = getTouchingToken(sourceFile, insertAtLineStart ? startPosition : position);
515        const indent = sourceFile.text.slice(lineStartPosition, startPosition);
516        const text = `${insertAtLineStart ? "" : this.newLineCharacter}//${commentText}${this.newLineCharacter}${indent}`;
517        this.insertText(sourceFile, token.getStart(sourceFile), text);
518    }
519
520    public insertJsdocCommentBefore(sourceFile: SourceFile, node: HasJSDoc, tag: JSDoc): void {
521        const fnStart = node.getStart(sourceFile);
522        if (node.jsDoc) {
523            for (const jsdoc of node.jsDoc) {
524                this.deleteRange(sourceFile, {
525                    pos: getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile),
526                    end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {})
527                });
528            }
529        }
530        const startPosition = getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1);
531        const indent = sourceFile.text.slice(startPosition, fnStart);
532        this.insertNodeAt(sourceFile, fnStart, tag, { suffix: this.newLineCharacter + indent });
533    }
534
535    private createJSDocText(sourceFile: SourceFile, node: HasJSDoc) {
536        const comments = flatMap(node.jsDoc, jsDoc =>
537            isString(jsDoc.comment) ? factory.createJSDocText(jsDoc.comment) : jsDoc.comment) as JSDocComment[];
538        const jsDoc = singleOrUndefined(node.jsDoc);
539        return jsDoc && positionsAreOnSameLine(jsDoc.pos, jsDoc.end, sourceFile) && length(comments) === 0 ? undefined :
540            factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n")));
541    }
542
543    public replaceJSDocComment(sourceFile: SourceFile, node: HasJSDoc, tags: readonly JSDocTag[]) {
544        this.insertJsdocCommentBefore(sourceFile, updateJSDocHost(node), factory.createJSDocComment(this.createJSDocText(sourceFile, node), factory.createNodeArray(tags)));
545    }
546
547    public addJSDocTags(sourceFile: SourceFile, parent: HasJSDoc, newTags: readonly JSDocTag[]): void {
548        const oldTags = flatMapToMutable(parent.jsDoc, j => j.tags);
549        const unmergedNewTags = newTags.filter(newTag => !oldTags.some((tag, i) => {
550            const merged = tryMergeJsdocTags(tag, newTag);
551            if (merged) oldTags[i] = merged;
552            return !!merged;
553        }));
554        this.replaceJSDocComment(sourceFile, parent, [...oldTags, ...unmergedNewTags]);
555    }
556
557    public filterJSDocTags(sourceFile: SourceFile, parent: HasJSDoc, predicate: (tag: JSDocTag) => boolean): void {
558        this.replaceJSDocComment(sourceFile, parent, filter(flatMapToMutable(parent.jsDoc, j => j.tags), predicate));
559    }
560
561    public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string): void {
562        this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text });
563    }
564
565    public insertText(sourceFile: SourceFile, pos: number, text: string): void {
566        this.replaceRangeWithText(sourceFile, createRange(pos), text);
567    }
568
569    /** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */
570    public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): boolean {
571        let endNode: Node | undefined;
572        if (isFunctionLike(node)) {
573            endNode = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile);
574            if (!endNode) {
575                if (!isArrowFunction(node)) return false; // Function missing parentheses, give up
576                // If no `)`, is an arrow function `x => x`, so use the end of the first parameter
577                endNode = first(node.parameters);
578            }
579        }
580        else {
581            endNode = (node.kind === SyntaxKind.VariableDeclaration ? node.exclamationToken : node.questionToken) ?? node.name;
582        }
583
584        this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
585        return true;
586    }
587
588    public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void {
589        const start = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1;
590        const suffix = node.parameters.length ? ", " : "";
591
592        this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix });
593    }
594
595    public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: readonly TypeParameterDeclaration[]): void {
596        // If no `(`, is an arrow function `x => x`, so use the pos of the first parameter
597        const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile);
598        this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">", joiner: ", " });
599    }
600
601    private getOptionsForInsertNodeBefore(before: Node, inserted: Node, blankLineBetween: boolean): InsertNodeOptions {
602        if (isStatement(before) || isClassElement(before)) {
603            return { suffix: blankLineBetween ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter };
604        }
605        else if (isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2;
606            return { suffix: ", " };
607        }
608        else if (isParameter(before)) {
609            return isParameter(inserted) ? { suffix: ", " } : {};
610        }
611        else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) {
612            return { suffix: ", " };
613        }
614        else if (isImportSpecifier(before)) {
615            return { suffix: "," + (blankLineBetween ? this.newLineCharacter : " ") };
616        }
617        return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it
618    }
619
620    public insertNodeAtConstructorStart(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void {
621        const firstStatement = firstOrUndefined(ctr.body!.statements);
622        if (!firstStatement || !ctr.body!.multiLine) {
623            this.replaceConstructorBody(sourceFile, ctr, [newStatement, ...ctr.body!.statements]);
624        }
625        else {
626            this.insertNodeBefore(sourceFile, firstStatement, newStatement);
627        }
628    }
629
630    public insertNodeAtConstructorStartAfterSuperCall(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void {
631        const superCallStatement = find(ctr.body!.statements, stmt => isExpressionStatement(stmt) && isSuperCall(stmt.expression));
632        if (!superCallStatement || !ctr.body!.multiLine) {
633            this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]);
634        }
635        else {
636            this.insertNodeAfter(sourceFile, superCallStatement, newStatement);
637        }
638    }
639
640    public insertNodeAtConstructorEnd(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void {
641        const lastStatement = lastOrUndefined(ctr.body!.statements);
642        if (!lastStatement || !ctr.body!.multiLine) {
643            this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]);
644        }
645        else {
646            this.insertNodeAfter(sourceFile, lastStatement, newStatement);
647        }
648    }
649
650    private replaceConstructorBody(sourceFile: SourceFile, ctr: ConstructorDeclaration, statements: readonly Statement[]): void {
651        this.replaceNode(sourceFile, ctr.body!, factory.createBlock(statements, /*multiLine*/ true));
652    }
653
654    public insertNodeAtEndOfScope(sourceFile: SourceFile, scope: Node, newNode: Node): void {
655        const pos = getAdjustedStartPosition(sourceFile, scope.getLastToken()!, {});
656        this.insertNodeAt(sourceFile, pos, newNode, {
657            prefix: isLineBreak(sourceFile.text.charCodeAt(scope.getLastToken()!.pos)) ? this.newLineCharacter : this.newLineCharacter + this.newLineCharacter,
658            suffix: this.newLineCharacter
659        });
660    }
661
662    public insertMemberAtStart(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, newElement: ClassElement | PropertySignature | MethodSignature): void {
663        this.insertNodeAtStartWorker(sourceFile, node, newElement);
664    }
665
666    public insertNodeAtObjectStart(sourceFile: SourceFile, obj: ObjectLiteralExpression, newElement: ObjectLiteralElementLike): void {
667        this.insertNodeAtStartWorker(sourceFile, obj, newElement);
668    }
669
670    private insertNodeAtStartWorker(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, newElement: ClassElement | ObjectLiteralElementLike | PropertySignature | MethodSignature): void {
671        const indentation = this.guessIndentationFromExistingMembers(sourceFile, node) ?? this.computeIndentationForNewMember(sourceFile, node);
672        this.insertNodeAt(sourceFile, getMembersOrProperties(node).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation));
673    }
674
675    /**
676     * Tries to guess the indentation from the existing members of a class/interface/object. All members must be on
677     * new lines and must share the same indentation.
678     */
679    private guessIndentationFromExistingMembers(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
680        let indentation: number | undefined;
681        let lastRange: TextRange = node;
682        for (const member of getMembersOrProperties(node)) {
683            if (rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) {
684                // each indented member must be on a new line
685                return undefined;
686            }
687            const memberStart = member.getStart(sourceFile);
688            const memberIndentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(memberStart, sourceFile), memberStart, sourceFile, this.formatContext.options);
689            if (indentation === undefined) {
690                indentation = memberIndentation;
691            }
692            else if (memberIndentation !== indentation) {
693                // indentation of multiple members is not consistent
694                return undefined;
695            }
696            lastRange = member;
697        }
698        return indentation;
699    }
700
701    private computeIndentationForNewMember(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
702        const nodeStart = node.getStart(sourceFile);
703        return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(nodeStart, sourceFile), nodeStart, sourceFile, this.formatContext.options)
704            + (this.formatContext.options.indentSize ?? 4);
705    }
706
707    private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, indentation: number): InsertNodeOptions {
708        // Rules:
709        // - Always insert leading newline.
710        // - For object literals:
711        //   - Add a trailing comma if there are existing members in the node, or the source file is not a JSON file
712        //     (because trailing commas are generally illegal in a JSON file).
713        //   - Add a leading comma if the source file is not a JSON file, there are existing insertions,
714        //     and the node is empty (because we didn't add a trailing comma per the previous rule).
715        // - Only insert a trailing newline if body is single-line and there are no other insertions for the node.
716        //   NOTE: This is handled in `finishClassesWithNodesInsertedAtStart`.
717
718        const members = getMembersOrProperties(node);
719        const isEmpty = members.length === 0;
720        const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(node), { node, sourceFile });
721        const insertTrailingComma = isObjectLiteralExpression(node) && (!isJsonSourceFile(sourceFile) || !isEmpty);
722        const insertLeadingComma = isObjectLiteralExpression(node) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion;
723        return {
724            indentation,
725            prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter,
726            suffix: insertTrailingComma ? "," : isInterfaceDeclaration(node) && isEmpty ? ";" : ""
727        };
728    }
729
730    public insertNodeAfterComma(sourceFile: SourceFile, after: Node, newNode: Node): void {
731        const endPosition = this.insertNodeAfterWorker(sourceFile, this.nextCommaToken(sourceFile, after) || after, newNode);
732        this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after));
733    }
734
735    public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void {
736        const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode);
737        this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after));
738    }
739
740    public insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray<Node>, newNode: Node): void {
741        this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " });
742    }
743
744    public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: readonly Node[]): void {
745        const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes));
746        this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after));
747    }
748
749    private insertNodeAfterWorker(sourceFile: SourceFile, after: Node, newNode: Node): number {
750        if (needSemicolonBetween(after, newNode)) {
751            // check if previous statement ends with semicolon
752            // if not - insert semicolon to preserve the code from changing the meaning due to ASI
753            if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) {
754                this.replaceRange(sourceFile, createRange(after.end), factory.createToken(SyntaxKind.SemicolonToken));
755            }
756        }
757        const endPosition = getAdjustedEndPosition(sourceFile, after, {});
758        return endPosition;
759    }
760
761    private getInsertNodeAfterOptions(sourceFile: SourceFile, after: Node): InsertNodeOptions {
762        const options = this.getInsertNodeAfterOptionsWorker(after);
763        return {
764            ...options,
765            prefix: after.end === sourceFile.end && isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix,
766        };
767    }
768
769    private getInsertNodeAfterOptionsWorker(node: Node): InsertNodeOptions {
770        switch (node.kind) {
771            case SyntaxKind.ClassDeclaration:
772            case SyntaxKind.StructDeclaration:
773            case SyntaxKind.ModuleDeclaration:
774                return { prefix: this.newLineCharacter, suffix: this.newLineCharacter };
775
776            case SyntaxKind.VariableDeclaration:
777            case SyntaxKind.StringLiteral:
778            case SyntaxKind.Identifier:
779                return { prefix: ", " };
780
781            case SyntaxKind.PropertyAssignment:
782                return { suffix: "," + this.newLineCharacter };
783
784            case SyntaxKind.ExportKeyword:
785                return { prefix: " " };
786
787            case SyntaxKind.Parameter:
788                return {};
789
790            default:
791                Debug.assert(isStatement(node) || isClassOrTypeElement(node)); // Else we haven't handled this kind of node yet -- add it
792                return { suffix: this.newLineCharacter };
793        }
794    }
795
796    public insertName(sourceFile: SourceFile, node: FunctionExpression | ClassExpression | ArrowFunction, name: string): void {
797        Debug.assert(!node.name);
798        if (node.kind === SyntaxKind.ArrowFunction) {
799            const arrow = findChildOfKind(node, SyntaxKind.EqualsGreaterThanToken, sourceFile)!;
800            const lparen = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile);
801            if (lparen) {
802                // `() => {}` --> `function f() {}`
803                this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [factory.createToken(SyntaxKind.FunctionKeyword), factory.createIdentifier(name)], { joiner: " " });
804                deleteNode(this, sourceFile, arrow);
805            }
806            else {
807                // `x => {}` -> `function f(x) {}`
808                this.insertText(sourceFile, first(node.parameters).getStart(sourceFile), `function ${name}(`);
809                // Replacing full range of arrow to get rid of the leading space -- replace ` =>` with `)`
810                this.replaceRange(sourceFile, arrow, factory.createToken(SyntaxKind.CloseParenToken));
811            }
812
813            if (node.body.kind !== SyntaxKind.Block) {
814                // `() => 0` => `function f() { return 0; }`
815                this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [factory.createToken(SyntaxKind.OpenBraceToken), factory.createToken(SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " });
816                this.insertNodesAt(sourceFile, node.body.end, [factory.createToken(SyntaxKind.SemicolonToken), factory.createToken(SyntaxKind.CloseBraceToken)], { joiner: " " });
817            }
818        }
819        else {
820            const pos = findChildOfKind(node, node.kind === SyntaxKind.FunctionExpression ? SyntaxKind.FunctionKeyword : SyntaxKind.ClassKeyword, sourceFile)!.end;
821            this.insertNodeAt(sourceFile, pos, factory.createIdentifier(name), { prefix: " " });
822        }
823    }
824
825    public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void {
826        this.insertText(sourceFile, node.getStart(sourceFile), "export ");
827    }
828
829    public insertImportSpecifierAtIndex(sourceFile: SourceFile, importSpecifier: ImportSpecifier, namedImports: NamedImports, index: number) {
830        const prevSpecifier = namedImports.elements[index - 1];
831        if (prevSpecifier) {
832            this.insertNodeInListAfter(sourceFile, prevSpecifier, importSpecifier);
833        }
834        else {
835            this.insertNodeBefore(
836                sourceFile,
837                namedImports.elements[0],
838                importSpecifier,
839                !positionsAreOnSameLine(namedImports.elements[0].getStart(), namedImports.parent.parent.getStart(), sourceFile));
840        }
841    }
842
843    /**
844     * This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
845     * i.e. arguments in arguments lists, parameters in parameter lists etc.
846     * Note that separators are part of the node in statements and class elements.
847     */
848    public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node, containingList = formatting.SmartIndenter.getContainingList(after, sourceFile)): void {
849        if (!containingList) {
850            Debug.fail("node is not a list element");
851            return;
852        }
853        const index = indexOfNode(containingList, after);
854        if (index < 0) {
855            return;
856        }
857        const end = after.getEnd();
858        if (index !== containingList.length - 1) {
859            // any element except the last one
860            // use next sibling as an anchor
861            const nextToken = getTokenAtPosition(sourceFile, after.end);
862            if (nextToken && isSeparator(after, nextToken)) {
863                // for list
864                // a, b, c
865                // create change for adding 'e' after 'a' as
866                // - find start of next element after a (it is b)
867                // - use next element start as start and end position in final change
868                // - build text of change by formatting the text of node + whitespace trivia of b
869
870                // in multiline case it will work as
871                //   a,
872                //   b,
873                //   c,
874                // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#')
875                //   a,
876                //   insertedtext<separator>#
877                // ###b,
878                //   c,
879                const nextNode = containingList[index + 1];
880                const startPos = skipWhitespacesAndLineBreaks(sourceFile.text, nextNode.getFullStart());
881
882                // write separator and leading trivia of the next element as suffix
883                const suffix = `${tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, startPos)}`;
884                this.insertNodesAt(sourceFile, startPos, [newNode], { suffix });
885            }
886        }
887        else {
888            const afterStart = after.getStart(sourceFile);
889            const afterStartLinePosition = getLineStartPositionForPosition(afterStart, sourceFile);
890
891            let separator: SyntaxKind.CommaToken | SyntaxKind.SemicolonToken | undefined;
892            let multilineList = false;
893
894            // insert element after the last element in the list that has more than one item
895            // pick the element preceding the after element to:
896            // - pick the separator
897            // - determine if list is a multiline
898            if (containingList.length === 1) {
899                // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise
900                // i.e. var x = 1 // this is x
901                //     | new element will be inserted at this position
902                separator = SyntaxKind.CommaToken;
903            }
904            else {
905                // element has more than one element, pick separator from the list
906                const tokenBeforeInsertPosition = findPrecedingToken(after.pos, sourceFile);
907                separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : SyntaxKind.CommaToken;
908                // determine if list is multiline by checking lines of after element and element that precedes it.
909                const afterMinusOneStartLinePosition = getLineStartPositionForPosition(containingList[index - 1].getStart(sourceFile), sourceFile);
910                multilineList = afterMinusOneStartLinePosition !== afterStartLinePosition;
911            }
912            if (hasCommentsBeforeLineBreak(sourceFile.text, after.end)) {
913                // in this case we'll always treat containing list as multiline
914                multilineList = true;
915            }
916            if (multilineList) {
917                // insert separator immediately following the 'after' node to preserve comments in trailing trivia
918                this.replaceRange(sourceFile, createRange(end), factory.createToken(separator));
919                // use the same indentation as 'after' item
920                const indentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.formatContext.options);
921                // insert element before the line break on the line that contains 'after' element
922                let insertPos = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false);
923                // find position before "\n" or "\r\n"
924                while (insertPos !== end && isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) {
925                    insertPos--;
926                }
927                this.replaceRange(sourceFile, createRange(insertPos), newNode, { indentation, prefix: this.newLineCharacter });
928            }
929            else {
930                this.replaceRange(sourceFile, createRange(end), newNode, { prefix: `${tokenToString(separator)} ` });
931            }
932        }
933    }
934
935    public parenthesizeExpression(sourceFile: SourceFile, expression: Expression) {
936        this.replaceRange(sourceFile, rangeOfNode(expression), factory.createParenthesizedExpression(expression));
937    }
938
939    private finishClassesWithNodesInsertedAtStart(): void {
940        this.classesWithNodesInsertedAtStart.forEach(({ node, sourceFile }) => {
941            const [openBraceEnd, closeBraceEnd] = getClassOrObjectBraceEnds(node, sourceFile);
942            if (openBraceEnd !== undefined && closeBraceEnd !== undefined) {
943                const isEmpty = getMembersOrProperties(node).length === 0;
944                const isSingleLine = positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile);
945                if (isEmpty && isSingleLine && openBraceEnd !== closeBraceEnd - 1) {
946                    // For `class C { }` remove the whitespace inside the braces.
947                    this.deleteRange(sourceFile, createRange(openBraceEnd, closeBraceEnd - 1));
948                }
949                if (isSingleLine) {
950                    this.insertText(sourceFile, closeBraceEnd - 1, this.newLineCharacter);
951                }
952            }
953        });
954    }
955
956    private finishDeleteDeclarations(): void {
957        const deletedNodesInLists = new Set<Node>(); // Stores nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
958        for (const { sourceFile, node } of this.deletedNodes) {
959            if (!this.deletedNodes.some(d => d.sourceFile === sourceFile && rangeContainsRangeExclusive(d.node, node))) {
960                if (isArray(node)) {
961                    this.deleteRange(sourceFile, rangeOfTypeParameters(sourceFile, node));
962                }
963                else {
964                    deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node);
965                }
966            }
967        }
968
969        deletedNodesInLists.forEach(node => {
970            const sourceFile = node.getSourceFile();
971            const list = formatting.SmartIndenter.getContainingList(node, sourceFile)!;
972            if (node !== last(list)) return;
973
974            const lastNonDeletedIndex = findLastIndex(list, n => !deletedNodesInLists.has(n), list.length - 2);
975            if (lastNonDeletedIndex !== -1) {
976                this.deleteRange(sourceFile, { pos: list[lastNonDeletedIndex].end, end: startPositionToDeleteNodeInList(sourceFile, list[lastNonDeletedIndex + 1]) });
977            }
978        });
979    }
980
981    /**
982     * Note: after calling this, the TextChanges object must be discarded!
983     * @param validate only for tests
984     *    The reason we must validate as part of this method is that `getNonFormattedText` changes the node's positions,
985     *    so we can only call this once and can't get the non-formatted text separately.
986     */
987    public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
988        this.finishDeleteDeclarations();
989        this.finishClassesWithNodesInsertedAtStart();
990        const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
991        for (const { oldFile, fileName, statements } of this.newFiles) {
992            changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext));
993        }
994        return changes;
995    }
996
997    public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void {
998        this.newFiles.push({ oldFile, fileName, statements });
999    }
1000}
1001
1002function updateJSDocHost(parent: HasJSDoc): HasJSDoc {
1003    if (parent.kind !== SyntaxKind.ArrowFunction) {
1004        return parent;
1005    }
1006    const jsDocNode = parent.parent.kind === SyntaxKind.PropertyDeclaration ?
1007        parent.parent as HasJSDoc :
1008        parent.parent.parent as HasJSDoc;
1009    jsDocNode.jsDoc = parent.jsDoc;
1010    jsDocNode.jsDocCache = parent.jsDocCache;
1011    return jsDocNode;
1012}
1013
1014function tryMergeJsdocTags(oldTag: JSDocTag, newTag: JSDocTag): JSDocTag | undefined {
1015    if (oldTag.kind !== newTag.kind) {
1016        return undefined;
1017    }
1018    switch (oldTag.kind) {
1019        case SyntaxKind.JSDocParameterTag: {
1020            const oldParam = oldTag as JSDocParameterTag;
1021            const newParam = newTag as JSDocParameterTag;
1022            return isIdentifier(oldParam.name) && isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText
1023                ? factory.createJSDocParameterTag(/*tagName*/ undefined, newParam.name, /*isBracketed*/ false, newParam.typeExpression, newParam.isNameFirst, oldParam.comment)
1024                : undefined;
1025        }
1026        case SyntaxKind.JSDocReturnTag:
1027            return factory.createJSDocReturnTag(/*tagName*/ undefined, (newTag as JSDocReturnTag).typeExpression, oldTag.comment);
1028        case SyntaxKind.JSDocTypeTag:
1029            return factory.createJSDocTypeTag(/*tagName*/ undefined, (newTag as JSDocTypeTag).typeExpression, oldTag.comment);
1030    }
1031}
1032
1033// find first non-whitespace position in the leading trivia of the node
1034function startPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node): number {
1035    return skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.IncludeAll }), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
1036}
1037
1038function endPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node, prevNode: Node | undefined, nextNode: Node): number {
1039    const end = startPositionToDeleteNodeInList(sourceFile, nextNode);
1040    if (prevNode === undefined || positionsAreOnSameLine(getAdjustedEndPosition(sourceFile, node, {}), end, sourceFile)) {
1041        return end;
1042    }
1043    const token = findPrecedingToken(nextNode.getStart(sourceFile), sourceFile);
1044    if (isSeparator(node, token)) {
1045        const prevToken = findPrecedingToken(node.getStart(sourceFile), sourceFile);
1046        if (isSeparator(prevNode, prevToken)) {
1047            const pos = skipTrivia(sourceFile.text, token.getEnd(), /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
1048            if (positionsAreOnSameLine(prevToken.getStart(sourceFile), token.getStart(sourceFile), sourceFile)) {
1049                return isLineBreak(sourceFile.text.charCodeAt(pos - 1)) ? pos - 1 : pos;
1050            }
1051            if (isLineBreak(sourceFile.text.charCodeAt(pos))) {
1052                return pos;
1053            }
1054        }
1055    }
1056    return end;
1057}
1058
1059function getClassOrObjectBraceEnds(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, sourceFile: SourceFile): [number | undefined, number | undefined] {
1060    const open = findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile);
1061    const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile);
1062    return [open?.end, close?.end];
1063}
1064function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode): NodeArray<Node> {
1065    return isObjectLiteralExpression(node) ? node.properties : node.members;
1066}
1067
1068/** @internal */
1069export type ValidateNonFormattedText = (node: Node, text: string) => void;
1070
1071/** @internal */
1072export function getNewFileText(statements: readonly Statement[], scriptKind: ScriptKind, newLineCharacter: string, formatContext: formatting.FormatContext): string {
1073    return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext);
1074}
1075
1076namespace changesToText {
1077    export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] {
1078        return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => {
1079            const sourceFile = changesInFile[0].sourceFile;
1080            // order changes by start position
1081            // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa.
1082            const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end));
1083            // verify that change intervals do not overlap, except possibly at end points.
1084            for (let i = 0; i < normalized.length - 1; i++) {
1085                Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () =>
1086                    `${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`);
1087            }
1088
1089            const textChanges = mapDefined(normalized, c => {
1090                const span = createTextSpanFromRange(c.range);
1091                const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate);
1092
1093                // Filter out redundant changes.
1094                if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) {
1095                    return undefined;
1096                }
1097
1098                return createTextChange(span, newText);
1099            });
1100
1101            return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined;
1102        });
1103    }
1104
1105    export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges {
1106        const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext);
1107        return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true };
1108    }
1109
1110    export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): string {
1111        // TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this
1112        const nonFormattedText = statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter);
1113        const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind);
1114        const changes = formatting.formatDocument(sourceFile, formatContext);
1115        return applyChanges(nonFormattedText, changes) + newLineCharacter;
1116    }
1117
1118    function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string {
1119        if (change.kind === ChangeKind.Remove) {
1120            return "";
1121        }
1122        if (change.kind === ChangeKind.Text) {
1123            return change.text;
1124        }
1125
1126        const { options = {}, range: { pos } } = change;
1127        const format = (n: Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate);
1128        const text = change.kind === ChangeKind.ReplaceWithMultipleNodes
1129            ? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options?.joiner || newLineCharacter)
1130            : format(change.node);
1131        // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
1132        const noIndent = (options.indentation !== undefined || getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, "");
1133        return (options.prefix || "") + noIndent
1134             + ((!options.suffix || endsWith(noIndent, options.suffix))
1135                ? "" : options.suffix);
1136    }
1137
1138    /** Note: this may mutate `nodeIn`. */
1139    function getFormattedTextOfNode(nodeIn: Node, sourceFile: SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string {
1140        const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter);
1141        if (validate) validate(node, text);
1142        const formatOptions = getFormatCodeSettingsForWriting(formatContext, sourceFile);
1143        const initialIndentation =
1144            indentation !== undefined
1145                ? indentation
1146                : formatting.SmartIndenter.getIndentation(pos, sourceFile, formatOptions, prefix === newLineCharacter || getLineStartPositionForPosition(pos, sourceFile) === pos);
1147        if (delta === undefined) {
1148            delta = formatting.SmartIndenter.shouldIndentChildNode(formatOptions, nodeIn) ? (formatOptions.indentSize || 0) : 0;
1149        }
1150
1151        const file: SourceFileLike = {
1152            text,
1153            getLineAndCharacterOfPosition(pos) {
1154                return getLineAndCharacterOfPosition(this, pos);
1155            }
1156        };
1157        const changes = formatting.formatNodeGivenIndentation(node, file, sourceFile.languageVariant, initialIndentation, delta, { ...formatContext, options: formatOptions });
1158        return applyChanges(text, changes);
1159    }
1160
1161    /** Note: output node may be mutated input node. */
1162    export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } {
1163        const writer = createWriter(newLineCharacter);
1164        const newLine = getNewLineKind(newLineCharacter);
1165        createPrinter({
1166            newLine,
1167            neverAsciiEscape: true,
1168            preserveSourceNewlines: true,
1169            terminateUnterminatedLiterals: true
1170        }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer);
1171        return { text: writer.getText(), node: assignPositionsToNode(node) };
1172    }
1173}
1174
1175/** @internal */
1176export function applyChanges(text: string, changes: readonly TextChange[]): string {
1177    for (let i = changes.length - 1; i >= 0; i--) {
1178        const { span, newText } = changes[i];
1179        text = `${text.substring(0, span.start)}${newText}${text.substring(textSpanEnd(span))}`;
1180    }
1181    return text;
1182}
1183
1184function isTrivia(s: string) {
1185    return skipTrivia(s, 0) === s.length;
1186}
1187
1188// A transformation context that won't perform parenthesization, as some parenthesization rules
1189// are more aggressive than is strictly necessary.
1190const textChangesTransformationContext: TransformationContext = {
1191    ...nullTransformationContext,
1192    factory: createNodeFactory(
1193        nullTransformationContext.factory.flags | NodeFactoryFlags.NoParenthesizerRules,
1194        nullTransformationContext.factory.baseFactory),
1195};
1196
1197/** @internal */
1198export function assignPositionsToNode(node: Node): Node {
1199    const visited = visitEachChild(node, assignPositionsToNode, textChangesTransformationContext, assignPositionsToNodeArray, assignPositionsToNode);
1200    // create proxy node for non synthesized nodes
1201    const newNode = nodeIsSynthesized(visited) ? visited : Object.create(visited) as Node;
1202    setTextRangePosEnd(newNode, getPos(node), getEnd(node));
1203    return newNode;
1204}
1205
1206function assignPositionsToNodeArray(nodes: NodeArray<any>, visitor: Visitor, test?: (node: Node) => boolean, start?: number, count?: number) {
1207    const visited = visitNodes(nodes, visitor, test, start, count);
1208    if (!visited) {
1209        return visited;
1210    }
1211    // clone nodearray if necessary
1212    const nodeArray = visited === nodes ? factory.createNodeArray(visited.slice(0)) : visited;
1213    setTextRangePosEnd(nodeArray, getPos(nodes), getEnd(nodes));
1214    return nodeArray;
1215}
1216
1217/** @internal */
1218export interface TextChangesWriter extends EmitTextWriter, PrintHandlers {}
1219
1220/** @internal */
1221export function createWriter(newLine: string): TextChangesWriter {
1222    let lastNonTriviaPosition = 0;
1223
1224    const writer = createTextWriter(newLine);
1225    const onBeforeEmitNode: PrintHandlers["onBeforeEmitNode"] = node => {
1226        if (node) {
1227            setPos(node, lastNonTriviaPosition);
1228        }
1229    };
1230    const onAfterEmitNode: PrintHandlers["onAfterEmitNode"] = node => {
1231        if (node) {
1232            setEnd(node, lastNonTriviaPosition);
1233        }
1234    };
1235    const onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"] = nodes => {
1236        if (nodes) {
1237            setPos(nodes, lastNonTriviaPosition);
1238        }
1239    };
1240    const onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"] = nodes => {
1241        if (nodes) {
1242            setEnd(nodes, lastNonTriviaPosition);
1243        }
1244    };
1245    const onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"] = node => {
1246        if (node) {
1247            setPos(node, lastNonTriviaPosition);
1248        }
1249    };
1250    const onAfterEmitToken: PrintHandlers["onAfterEmitToken"] = node => {
1251        if (node) {
1252            setEnd(node, lastNonTriviaPosition);
1253        }
1254    };
1255
1256    function setLastNonTriviaPosition(s: string, force: boolean) {
1257        if (force || !isTrivia(s)) {
1258            lastNonTriviaPosition = writer.getTextPos();
1259            let i = 0;
1260            while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) {
1261                i++;
1262            }
1263            // trim trailing whitespaces
1264            lastNonTriviaPosition -= i;
1265        }
1266    }
1267
1268    function write(s: string): void {
1269        writer.write(s);
1270        setLastNonTriviaPosition(s, /*force*/ false);
1271    }
1272    function writeComment(s: string): void {
1273        writer.writeComment(s);
1274    }
1275    function writeKeyword(s: string): void {
1276        writer.writeKeyword(s);
1277        setLastNonTriviaPosition(s, /*force*/ false);
1278    }
1279    function writeOperator(s: string): void {
1280        writer.writeOperator(s);
1281        setLastNonTriviaPosition(s, /*force*/ false);
1282    }
1283    function writePunctuation(s: string): void {
1284        writer.writePunctuation(s);
1285        setLastNonTriviaPosition(s, /*force*/ false);
1286    }
1287    function writeTrailingSemicolon(s: string): void {
1288        writer.writeTrailingSemicolon(s);
1289        setLastNonTriviaPosition(s, /*force*/ false);
1290    }
1291    function writeParameter(s: string): void {
1292        writer.writeParameter(s);
1293        setLastNonTriviaPosition(s, /*force*/ false);
1294    }
1295    function writeProperty(s: string): void {
1296        writer.writeProperty(s);
1297        setLastNonTriviaPosition(s, /*force*/ false);
1298    }
1299    function writeSpace(s: string): void {
1300        writer.writeSpace(s);
1301        setLastNonTriviaPosition(s, /*force*/ false);
1302    }
1303    function writeStringLiteral(s: string): void {
1304        writer.writeStringLiteral(s);
1305        setLastNonTriviaPosition(s, /*force*/ false);
1306    }
1307    function writeSymbol(s: string, sym: Symbol): void {
1308        writer.writeSymbol(s, sym);
1309        setLastNonTriviaPosition(s, /*force*/ false);
1310    }
1311    function writeLine(force?: boolean): void {
1312        writer.writeLine(force);
1313    }
1314    function increaseIndent(): void {
1315        writer.increaseIndent();
1316    }
1317    function decreaseIndent(): void {
1318        writer.decreaseIndent();
1319    }
1320    function getText(): string {
1321        return writer.getText();
1322    }
1323    function rawWrite(s: string): void {
1324        writer.rawWrite(s);
1325        setLastNonTriviaPosition(s, /*force*/ false);
1326    }
1327    function writeLiteral(s: string): void {
1328        writer.writeLiteral(s);
1329        setLastNonTriviaPosition(s, /*force*/ true);
1330    }
1331    function getTextPos(): number {
1332        return writer.getTextPos();
1333    }
1334    function getLine(): number {
1335        return writer.getLine();
1336    }
1337    function getColumn(): number {
1338        return writer.getColumn();
1339    }
1340    function getIndent(): number {
1341        return writer.getIndent();
1342    }
1343    function isAtStartOfLine(): boolean {
1344        return writer.isAtStartOfLine();
1345    }
1346    function clear(): void {
1347        writer.clear();
1348        lastNonTriviaPosition = 0;
1349    }
1350
1351    return {
1352        onBeforeEmitNode,
1353        onAfterEmitNode,
1354        onBeforeEmitNodeArray,
1355        onAfterEmitNodeArray,
1356        onBeforeEmitToken,
1357        onAfterEmitToken,
1358        write,
1359        writeComment,
1360        writeKeyword,
1361        writeOperator,
1362        writePunctuation,
1363        writeTrailingSemicolon,
1364        writeParameter,
1365        writeProperty,
1366        writeSpace,
1367        writeStringLiteral,
1368        writeSymbol,
1369        writeLine,
1370        increaseIndent,
1371        decreaseIndent,
1372        getText,
1373        rawWrite,
1374        writeLiteral,
1375        getTextPos,
1376        getLine,
1377        getColumn,
1378        getIndent,
1379        isAtStartOfLine,
1380        hasTrailingComment: () => writer.hasTrailingComment(),
1381        hasTrailingWhitespace: () => writer.hasTrailingWhitespace(),
1382        clear
1383    };
1384}
1385
1386function getInsertionPositionAtSourceFileTop(sourceFile: SourceFile): number {
1387    let lastPrologue: PrologueDirective | undefined;
1388    for (const node of sourceFile.statements) {
1389        if (isPrologueDirective(node)) {
1390            lastPrologue = node;
1391        }
1392        else {
1393            break;
1394        }
1395    }
1396
1397    let position = 0;
1398    const text = sourceFile.text;
1399    if (lastPrologue) {
1400        position = lastPrologue.end;
1401        advancePastLineBreak();
1402        return position;
1403    }
1404
1405    const shebang = getShebang(text);
1406    if (shebang !== undefined) {
1407        position = shebang.length;
1408        advancePastLineBreak();
1409    }
1410
1411    const ranges = getLeadingCommentRanges(text, position);
1412    if (!ranges) return position;
1413
1414    // Find the first attached comment to the first node and add before it
1415    let lastComment: { range: CommentRange; pinnedOrTripleSlash: boolean; } | undefined;
1416    let firstNodeLine: number | undefined;
1417    for (const range of ranges) {
1418        if (range.kind === SyntaxKind.MultiLineCommentTrivia) {
1419            if (isPinnedComment(text, range.pos)) {
1420                lastComment = { range, pinnedOrTripleSlash: true };
1421                continue;
1422            }
1423        }
1424        else if (isRecognizedTripleSlashComment(text, range.pos, range.end)) {
1425            lastComment = { range, pinnedOrTripleSlash: true };
1426            continue;
1427        }
1428
1429        if (lastComment) {
1430            // Always insert after pinned or triple slash comments
1431            if (lastComment.pinnedOrTripleSlash) break;
1432
1433            // There was a blank line between the last comment and this comment.
1434            // This comment is not part of the copyright comments
1435            const commentLine = sourceFile.getLineAndCharacterOfPosition(range.pos).line;
1436            const lastCommentEndLine = sourceFile.getLineAndCharacterOfPosition(lastComment.range.end).line;
1437            if (commentLine >= lastCommentEndLine + 2) break;
1438        }
1439
1440        if (sourceFile.statements.length) {
1441            if (firstNodeLine === undefined) firstNodeLine = sourceFile.getLineAndCharacterOfPosition(sourceFile.statements[0].getStart()).line;
1442            const commentEndLine = sourceFile.getLineAndCharacterOfPosition(range.end).line;
1443            if (firstNodeLine < commentEndLine + 2) break;
1444        }
1445        lastComment = { range, pinnedOrTripleSlash: false };
1446    }
1447
1448    if (lastComment) {
1449        position = lastComment.range.end;
1450        advancePastLineBreak();
1451    }
1452    return position;
1453
1454    function advancePastLineBreak() {
1455        if (position < text.length) {
1456            const charCode = text.charCodeAt(position);
1457            if (isLineBreak(charCode)) {
1458                position++;
1459
1460                if (position < text.length && charCode === CharacterCodes.carriageReturn && text.charCodeAt(position) === CharacterCodes.lineFeed) {
1461                    position++;
1462                }
1463            }
1464        }
1465    }
1466}
1467
1468/** @internal */
1469export function isValidLocationToAddComment(sourceFile: SourceFile, position: number) {
1470    return !isInComment(sourceFile, position) && !isInString(sourceFile, position) && !isInTemplateString(sourceFile, position) && !isInJSXText(sourceFile, position);
1471}
1472
1473function needSemicolonBetween(a: Node, b: Node): boolean {
1474    return (isPropertySignature(a) || isPropertyDeclaration(a)) && isClassOrTypeElement(b) && b.name!.kind === SyntaxKind.ComputedPropertyName
1475        || isStatementButNotDeclaration(a) && isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[`
1476}
1477
1478namespace deleteDeclaration {
1479    export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: Set<Node>, sourceFile: SourceFile, node: Node): void {
1480        switch (node.kind) {
1481            case SyntaxKind.Parameter: {
1482                const oldFunction = node.parent;
1483                if (isArrowFunction(oldFunction) &&
1484                    oldFunction.parameters.length === 1 &&
1485                    !findChildOfKind(oldFunction, SyntaxKind.OpenParenToken, sourceFile)) {
1486                    // Lambdas with exactly one parameter are special because, after removal, there
1487                    // must be an empty parameter list (i.e. `()`) and this won't necessarily be the
1488                    // case if the parameter is simply removed (e.g. in `x => 1`).
1489                    changes.replaceNodeWithText(sourceFile, node, "()");
1490                }
1491                else {
1492                    deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1493                }
1494                break;
1495            }
1496
1497            case SyntaxKind.ImportDeclaration:
1498            case SyntaxKind.ImportEqualsDeclaration:
1499                const isFirstImport = sourceFile.imports.length && node === first(sourceFile.imports).parent || node === find(sourceFile.statements, isAnyImportSyntax);
1500                // For first import, leave header comment in place, otherwise only delete JSDoc comments
1501                deleteNode(changes, sourceFile, node, {
1502                    leadingTriviaOption: isFirstImport ? LeadingTriviaOption.Exclude : hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine,
1503                });
1504                break;
1505
1506            case SyntaxKind.BindingElement:
1507                const pattern = (node as BindingElement).parent;
1508                const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && node !== last(pattern.elements);
1509                if (preserveComma) {
1510                    deleteNode(changes, sourceFile, node);
1511                }
1512                else {
1513                    deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1514                }
1515                break;
1516
1517            case SyntaxKind.VariableDeclaration:
1518                deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, node as VariableDeclaration);
1519                break;
1520
1521            case SyntaxKind.TypeParameter:
1522                deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1523                break;
1524
1525            case SyntaxKind.ImportSpecifier:
1526                const namedImports = (node as ImportSpecifier).parent;
1527                if (namedImports.elements.length === 1) {
1528                    deleteImportBinding(changes, sourceFile, namedImports);
1529                }
1530                else {
1531                    deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1532                }
1533                break;
1534
1535            case SyntaxKind.NamespaceImport:
1536                deleteImportBinding(changes, sourceFile, node as NamespaceImport);
1537                break;
1538
1539            case SyntaxKind.SemicolonToken:
1540                deleteNode(changes, sourceFile, node, { trailingTriviaOption: TrailingTriviaOption.Exclude });
1541                break;
1542
1543            case SyntaxKind.FunctionKeyword:
1544                deleteNode(changes, sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.Exclude });
1545                break;
1546
1547            case SyntaxKind.ClassDeclaration:
1548            case SyntaxKind.StructDeclaration:
1549            case SyntaxKind.FunctionDeclaration:
1550                deleteNode(changes, sourceFile, node, { leadingTriviaOption: hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine });
1551                break;
1552
1553            default:
1554                if (!node.parent) {
1555                    // a misbehaving client can reach here with the SourceFile node
1556                    deleteNode(changes, sourceFile, node);
1557                }
1558                else if (isImportClause(node.parent) && node.parent.name === node) {
1559                    deleteDefaultImport(changes, sourceFile, node.parent);
1560                }
1561                else if (isCallExpression(node.parent) && contains(node.parent.arguments, node)) {
1562                    deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1563                }
1564                else {
1565                    deleteNode(changes, sourceFile, node);
1566                }
1567        }
1568    }
1569
1570    function deleteDefaultImport(changes: ChangeTracker, sourceFile: SourceFile, importClause: ImportClause): void {
1571        if (!importClause.namedBindings) {
1572            // Delete the whole import
1573            deleteNode(changes, sourceFile, importClause.parent);
1574        }
1575        else {
1576            // import |d,| * as ns from './file'
1577            const start = importClause.name!.getStart(sourceFile);
1578            const nextToken = getTokenAtPosition(sourceFile, importClause.name!.end);
1579            if (nextToken && nextToken.kind === SyntaxKind.CommaToken) {
1580                // shift first non-whitespace position after comma to the start position of the node
1581                const end = skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true);
1582                changes.deleteRange(sourceFile, { pos: start, end });
1583            }
1584            else {
1585                deleteNode(changes, sourceFile, importClause.name!);
1586            }
1587        }
1588    }
1589
1590    function deleteImportBinding(changes: ChangeTracker, sourceFile: SourceFile, node: NamedImportBindings): void {
1591        if (node.parent.name) {
1592            // Delete named imports while preserving the default import
1593            // import d|, * as ns| from './file'
1594            // import d|, { a }| from './file'
1595            const previousToken = Debug.checkDefined(getTokenAtPosition(sourceFile, node.pos - 1));
1596            changes.deleteRange(sourceFile, { pos: previousToken.getStart(sourceFile), end: node.end });
1597        }
1598        else {
1599            // Delete the entire import declaration
1600            // |import * as ns from './file'|
1601            // |import { a } from './file'|
1602            const importDecl = getAncestor(node, SyntaxKind.ImportDeclaration)!;
1603            deleteNode(changes, sourceFile, importDecl);
1604        }
1605    }
1606
1607    function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: Set<Node>, sourceFile: SourceFile, node: VariableDeclaration): void {
1608        const { parent } = node;
1609
1610        if (parent.kind === SyntaxKind.CatchClause) {
1611            // TODO: There's currently no unused diagnostic for this, could be a suggestion
1612            changes.deleteNodeRange(sourceFile, findChildOfKind(parent, SyntaxKind.OpenParenToken, sourceFile)!, findChildOfKind(parent, SyntaxKind.CloseParenToken, sourceFile)!);
1613            return;
1614        }
1615
1616        if (parent.declarations.length !== 1) {
1617            deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1618            return;
1619        }
1620
1621        const gp = parent.parent;
1622        switch (gp.kind) {
1623            case SyntaxKind.ForOfStatement:
1624            case SyntaxKind.ForInStatement:
1625                changes.replaceNode(sourceFile, node, factory.createObjectLiteralExpression());
1626                break;
1627
1628            case SyntaxKind.ForStatement:
1629                deleteNode(changes, sourceFile, parent);
1630                break;
1631
1632            case SyntaxKind.VariableStatement:
1633                deleteNode(changes, sourceFile, gp, { leadingTriviaOption: hasJSDocNodes(gp) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine });
1634                break;
1635
1636            default:
1637                Debug.assertNever(gp);
1638        }
1639    }
1640}
1641
1642/**
1643 * Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`.
1644 *
1645 * @internal
1646 */
1647// Exported for tests only! (TODO: improve tests to not need this)
1648export function deleteNode(changes: ChangeTracker, sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
1649    const startPosition = getAdjustedStartPosition(sourceFile, node, options);
1650    const endPosition = getAdjustedEndPosition(sourceFile, node, options);
1651    changes.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
1652}
1653
1654function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: Set<Node>, sourceFile: SourceFile, node: Node): void {
1655    const containingList = Debug.checkDefined(formatting.SmartIndenter.getContainingList(node, sourceFile));
1656    const index = indexOfNode(containingList, node);
1657    Debug.assert(index !== -1);
1658    if (containingList.length === 1) {
1659        deleteNode(changes, sourceFile, node);
1660        return;
1661    }
1662
1663    // Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
1664    // That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`.
1665    Debug.assert(!deletedNodesInLists.has(node), "Deleting a node twice");
1666    deletedNodesInLists.add(node);
1667
1668    changes.deleteRange(sourceFile, {
1669        pos: startPositionToDeleteNodeInList(sourceFile, node),
1670        end: index === containingList.length - 1 ? getAdjustedEndPosition(sourceFile, node, {}) : endPositionToDeleteNodeInList(sourceFile, node, containingList[index - 1], containingList[index + 1]),
1671    });
1672}
1673