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