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