1/* @internal */ 2namespace ts.refactor.extractSymbol { 3 const refactorName = "Extract Symbol"; 4 5 const extractConstantAction = { 6 name: "Extract Constant", 7 description: getLocaleSpecificMessage(Diagnostics.Extract_constant), 8 kind: "refactor.extract.constant", 9 }; 10 const extractFunctionAction = { 11 name: "Extract Function", 12 description: getLocaleSpecificMessage(Diagnostics.Extract_function), 13 kind: "refactor.extract.function", 14 }; 15 registerRefactor(refactorName, { 16 kinds: [ 17 extractConstantAction.kind, 18 extractFunctionAction.kind 19 ], 20 getEditsForAction: getRefactorEditsToExtractSymbol, 21 getAvailableActions: getRefactorActionsToExtractSymbol, 22 }); 23 24 /** 25 * Compute the associated code actions 26 * Exported for tests. 27 */ 28 export function getRefactorActionsToExtractSymbol(context: RefactorContext): readonly ApplicableRefactorInfo[] { 29 const requestedRefactor = context.kind; 30 const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context), context.triggerReason === "invoked"); 31 const targetRange = rangeToExtract.targetRange; 32 33 if (targetRange === undefined) { 34 if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) { 35 return emptyArray; 36 } 37 38 const errors = []; 39 if (refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { 40 errors.push({ 41 name: refactorName, 42 description: extractFunctionAction.description, 43 actions: [{ ...extractFunctionAction, notApplicableReason: getStringError(rangeToExtract.errors) }] 44 }); 45 } 46 if (refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { 47 errors.push({ 48 name: refactorName, 49 description: extractConstantAction.description, 50 actions: [{ ...extractConstantAction, notApplicableReason: getStringError(rangeToExtract.errors) }] 51 }); 52 } 53 return errors; 54 } 55 56 const extractions = getPossibleExtractions(targetRange, context); 57 if (extractions === undefined) { 58 // No extractions possible 59 return emptyArray; 60 } 61 62 const functionActions: RefactorActionInfo[] = []; 63 const usedFunctionNames = new Map<string, boolean>(); 64 let innermostErrorFunctionAction: RefactorActionInfo | undefined; 65 66 const constantActions: RefactorActionInfo[] = []; 67 const usedConstantNames = new Map<string, boolean>(); 68 let innermostErrorConstantAction: RefactorActionInfo | undefined; 69 70 let i = 0; 71 for (const { functionExtraction, constantExtraction } of extractions) { 72 if (refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) { 73 const description = functionExtraction.description; 74 if (functionExtraction.errors.length === 0) { 75 // Don't issue refactorings with duplicated names. 76 // Scopes come back in "innermost first" order, so extractions will 77 // preferentially go into nearer scopes 78 if (!usedFunctionNames.has(description)) { 79 usedFunctionNames.set(description, true); 80 functionActions.push({ 81 description, 82 name: `function_scope_${i}`, 83 kind: extractFunctionAction.kind 84 }); 85 } 86 } 87 else if (!innermostErrorFunctionAction) { 88 innermostErrorFunctionAction = { 89 description, 90 name: `function_scope_${i}`, 91 notApplicableReason: getStringError(functionExtraction.errors), 92 kind: extractFunctionAction.kind 93 }; 94 } 95 } 96 97 if (refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) { 98 const description = constantExtraction.description; 99 if (constantExtraction.errors.length === 0) { 100 // Don't issue refactorings with duplicated names. 101 // Scopes come back in "innermost first" order, so extractions will 102 // preferentially go into nearer scopes 103 if (!usedConstantNames.has(description)) { 104 usedConstantNames.set(description, true); 105 constantActions.push({ 106 description, 107 name: `constant_scope_${i}`, 108 kind: extractConstantAction.kind 109 }); 110 } 111 } 112 else if (!innermostErrorConstantAction) { 113 innermostErrorConstantAction = { 114 description, 115 name: `constant_scope_${i}`, 116 notApplicableReason: getStringError(constantExtraction.errors), 117 kind: extractConstantAction.kind 118 }; 119 } 120 } 121 122 // *do* increment i anyway because we'll look for the i-th scope 123 // later when actually doing the refactoring if the user requests it 124 i++; 125 } 126 127 const infos: ApplicableRefactorInfo[] = []; 128 129 if (functionActions.length) { 130 infos.push({ 131 name: refactorName, 132 description: getLocaleSpecificMessage(Diagnostics.Extract_function), 133 actions: functionActions, 134 }); 135 } 136 else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) { 137 infos.push({ 138 name: refactorName, 139 description: getLocaleSpecificMessage(Diagnostics.Extract_function), 140 actions: [ innermostErrorFunctionAction ] 141 }); 142 } 143 144 if (constantActions.length) { 145 infos.push({ 146 name: refactorName, 147 description: getLocaleSpecificMessage(Diagnostics.Extract_constant), 148 actions: constantActions 149 }); 150 } 151 else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorConstantAction) { 152 infos.push({ 153 name: refactorName, 154 description: getLocaleSpecificMessage(Diagnostics.Extract_constant), 155 actions: [ innermostErrorConstantAction ] 156 }); 157 } 158 159 return infos.length ? infos : emptyArray; 160 161 function getStringError(errors: readonly Diagnostic[]) { 162 let error = errors[0].messageText; 163 if (typeof error !== "string") { 164 error = error.messageText; 165 } 166 return error; 167 } 168 } 169 170 /* Exported for tests */ 171 export function getRefactorEditsToExtractSymbol(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { 172 const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); 173 const targetRange = rangeToExtract.targetRange!; // TODO:GH#18217 174 175 const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName); 176 if (parsedFunctionIndexMatch) { 177 const index = +parsedFunctionIndexMatch[1]; 178 Debug.assert(isFinite(index), "Expected to parse a finite number from the function scope index"); 179 return getFunctionExtractionAtIndex(targetRange, context, index); 180 } 181 182 const parsedConstantIndexMatch = /^constant_scope_(\d+)$/.exec(actionName); 183 if (parsedConstantIndexMatch) { 184 const index = +parsedConstantIndexMatch[1]; 185 Debug.assert(isFinite(index), "Expected to parse a finite number from the constant scope index"); 186 return getConstantExtractionAtIndex(targetRange, context, index); 187 } 188 189 Debug.fail("Unrecognized action name"); 190 } 191 192 // Move these into diagnostic messages if they become user-facing 193 export namespace Messages { 194 function createMessage(message: string): DiagnosticMessage { 195 return { message, code: 0, category: DiagnosticCategory.Message, key: message }; 196 } 197 198 export const cannotExtractRange: DiagnosticMessage = createMessage("Cannot extract range."); 199 export const cannotExtractImport: DiagnosticMessage = createMessage("Cannot extract import statement."); 200 export const cannotExtractSuper: DiagnosticMessage = createMessage("Cannot extract super call."); 201 export const cannotExtractJSDoc: DiagnosticMessage = createMessage("Cannot extract JSDoc."); 202 export const cannotExtractEmpty: DiagnosticMessage = createMessage("Cannot extract empty range."); 203 export const expressionExpected: DiagnosticMessage = createMessage("expression expected."); 204 export const uselessConstantType: DiagnosticMessage = createMessage("No reason to extract constant of type."); 205 export const statementOrExpressionExpected: DiagnosticMessage = createMessage("Statement or expression expected."); 206 export const cannotExtractRangeContainingConditionalBreakOrContinueStatements: DiagnosticMessage = createMessage("Cannot extract range containing conditional break or continue statements."); 207 export const cannotExtractRangeContainingConditionalReturnStatement: DiagnosticMessage = createMessage("Cannot extract range containing conditional return statement."); 208 export const cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange: DiagnosticMessage = createMessage("Cannot extract range containing labeled break or continue with target outside of the range."); 209 export const cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators: DiagnosticMessage = createMessage("Cannot extract range containing writes to references located outside of the target range in generators."); 210 export const typeWillNotBeVisibleInTheNewScope = createMessage("Type will not visible in the new scope."); 211 export const functionWillNotBeVisibleInTheNewScope = createMessage("Function will not visible in the new scope."); 212 export const cannotExtractIdentifier = createMessage("Select more than a single identifier."); 213 export const cannotExtractExportedEntity = createMessage("Cannot extract exported declaration"); 214 export const cannotWriteInExpression = createMessage("Cannot write back side-effects when extracting an expression"); 215 export const cannotExtractReadonlyPropertyInitializerOutsideConstructor = createMessage("Cannot move initialization of read-only class property outside of the constructor"); 216 export const cannotExtractAmbientBlock = createMessage("Cannot extract code from ambient contexts"); 217 export const cannotAccessVariablesFromNestedScopes = createMessage("Cannot access variables from nested scopes"); 218 export const cannotExtractToJSClass = createMessage("Cannot extract constant to a class scope in JS"); 219 export const cannotExtractToExpressionArrowFunction = createMessage("Cannot extract constant to an arrow function without a block"); 220 export const cannotExtractFunctionsContainingThisToMethod = createMessage("Cannot extract functions containing this to method"); 221 } 222 223 enum RangeFacts { 224 None = 0, 225 HasReturn = 1 << 0, 226 IsGenerator = 1 << 1, 227 IsAsyncFunction = 1 << 2, 228 UsesThis = 1 << 3, 229 UsesThisInFunction = 1 << 4, 230 /** 231 * The range is in a function which needs the 'static' modifier in a class 232 */ 233 InStaticRegion = 1 << 5, 234 } 235 236 /** 237 * Represents an expression or a list of statements that should be extracted with some extra information 238 */ 239 interface TargetRange { 240 readonly range: Expression | Statement[]; 241 readonly facts: RangeFacts; 242 /** 243 * If `this` is referring to a function instead of class, we need to retrieve its type. 244 */ 245 readonly thisNode: Node | undefined; 246 } 247 248 /** 249 * Result of 'getRangeToExtract' operation: contains either a range or a list of errors 250 */ 251 type RangeToExtract = { 252 readonly targetRange?: never; 253 readonly errors: readonly Diagnostic[]; 254 } | { 255 readonly targetRange: TargetRange; 256 readonly errors?: never; 257 }; 258 259 /* 260 * Scopes that can store newly extracted method 261 */ 262 type Scope = FunctionLikeDeclaration | SourceFile | ModuleBlock | ClassLikeDeclaration; 263 264 /** 265 * getRangeToExtract takes a span inside a text file and returns either an expression or an array 266 * of statements representing the minimum set of nodes needed to extract the entire span. This 267 * process may fail, in which case a set of errors is returned instead. These errors are shown to 268 * users if they have the provideRefactorNotApplicableReason option set. 269 */ 270 // exported only for tests 271 export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan, invoked = true): RangeToExtract { 272 const { length } = span; 273 if (length === 0 && !invoked) { 274 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] }; 275 } 276 const cursorRequest = length === 0 && invoked; 277 278 const startToken = findFirstNonJsxWhitespaceToken(sourceFile, span.start); 279 const endToken = findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span)); 280 /* If the refactoring command is invoked through a keyboard action it's safe to assume that the user is actively looking for 281 refactoring actions at the span location. As they may not know the exact range that will trigger a refactoring, we expand the 282 searched span to cover a real node range making it more likely that something useful will show up. */ 283 const adjustedSpan = startToken && endToken && invoked ? getAdjustedSpanFromNodes(startToken, endToken, sourceFile) : span; 284 285 // Walk up starting from the the start position until we find a non-SourceFile node that subsumes the selected span. 286 // This may fail (e.g. you select two statements in the root of a source file) 287 const start = cursorRequest ? getExtractableParent(startToken) : getParentNodeInSpan(startToken, sourceFile, adjustedSpan); 288 289 // Do the same for the ending position 290 const end = cursorRequest ? start : getParentNodeInSpan(endToken, sourceFile, adjustedSpan); 291 292 // We'll modify these flags as we walk the tree to collect data 293 // about what things need to be done as part of the extraction. 294 let rangeFacts = RangeFacts.None; 295 296 let thisNode: Node | undefined; 297 298 if (!start || !end) { 299 // cannot find either start or end node 300 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; 301 } 302 303 if (start.flags & NodeFlags.JSDoc) { 304 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] }; 305 } 306 307 if (start.parent !== end.parent) { 308 // start and end nodes belong to different subtrees 309 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; 310 } 311 312 if (start !== end) { 313 // start and end should be statements and parent should be either block or a source file 314 if (!isBlockLike(start.parent)) { 315 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; 316 } 317 const statements: Statement[] = []; 318 for (const statement of start.parent.statements) { 319 if (statement === start || statements.length) { 320 const errors = checkNode(statement); 321 if (errors) { 322 return { errors }; 323 } 324 statements.push(statement); 325 } 326 if (statement === end) { 327 break; 328 } 329 } 330 331 if (!statements.length) { 332 // https://github.com/Microsoft/TypeScript/issues/20559 333 // Ranges like [|case 1: break;|] will fail to populate `statements` because 334 // they will never find `start` in `start.parent.statements`. 335 // Consider: We could support ranges like [|case 1:|] by refining them to just 336 // the expression. 337 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; 338 } 339 340 return { targetRange: { range: statements, facts: rangeFacts, thisNode } }; 341 } 342 343 if (isReturnStatement(start) && !start.expression) { 344 // Makes no sense to extract an expression-less return statement. 345 return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; 346 } 347 348 // We have a single node (start) 349 const node = refineNode(start); 350 351 const errors = checkRootNode(node) || checkNode(node); 352 if (errors) { 353 return { errors }; 354 } 355 return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, thisNode } }; // TODO: GH#18217 356 357 /** 358 * Attempt to refine the extraction node (generally, by shrinking it) to produce better results. 359 * @param node The unrefined extraction node. 360 */ 361 function refineNode(node: Node): Node { 362 if (isReturnStatement(node)) { 363 if (node.expression) { 364 return node.expression; 365 } 366 } 367 else if (isVariableStatement(node) || isVariableDeclarationList(node)) { 368 const declarations = isVariableStatement(node) ? node.declarationList.declarations : node.declarations; 369 let numInitializers = 0; 370 let lastInitializer: Expression | undefined; 371 for (const declaration of declarations) { 372 if (declaration.initializer) { 373 numInitializers++; 374 lastInitializer = declaration.initializer; 375 } 376 } 377 if (numInitializers === 1) { 378 return lastInitializer!; 379 } 380 // No special handling if there are multiple initializers. 381 } 382 else if (isVariableDeclaration(node)) { 383 if (node.initializer) { 384 return node.initializer; 385 } 386 } 387 return node; 388 } 389 390 function checkRootNode(node: Node): Diagnostic[] | undefined { 391 if (isIdentifier(isExpressionStatement(node) ? node.expression : node)) { 392 return [createDiagnosticForNode(node, Messages.cannotExtractIdentifier)]; 393 } 394 return undefined; 395 } 396 397 function checkForStaticContext(nodeToCheck: Node, containingClass: Node) { 398 let current: Node = nodeToCheck; 399 while (current !== containingClass) { 400 if (current.kind === SyntaxKind.PropertyDeclaration) { 401 if (isStatic(current)) { 402 rangeFacts |= RangeFacts.InStaticRegion; 403 } 404 break; 405 } 406 else if (current.kind === SyntaxKind.Parameter) { 407 const ctorOrMethod = getContainingFunction(current)!; 408 if (ctorOrMethod.kind === SyntaxKind.Constructor) { 409 rangeFacts |= RangeFacts.InStaticRegion; 410 } 411 break; 412 } 413 else if (current.kind === SyntaxKind.MethodDeclaration) { 414 if (isStatic(current)) { 415 rangeFacts |= RangeFacts.InStaticRegion; 416 } 417 } 418 current = current.parent; 419 } 420 } 421 422 // Verifies whether we can actually extract this node or not. 423 function checkNode(nodeToCheck: Node): Diagnostic[] | undefined { 424 const enum PermittedJumps { 425 None = 0, 426 Break = 1 << 0, 427 Continue = 1 << 1, 428 Return = 1 << 2 429 } 430 431 // We believe it's true because the node is from the (unmodified) tree. 432 Debug.assert(nodeToCheck.pos <= nodeToCheck.end, "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (1)"); 433 434 // For understanding how skipTrivia functioned: 435 Debug.assert(!positionIsSynthesized(nodeToCheck.pos), "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (2)"); 436 437 if (!isStatement(nodeToCheck) && !(isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck)) && !isStringLiteralJsxAttribute(nodeToCheck)) { 438 return [createDiagnosticForNode(nodeToCheck, Messages.statementOrExpressionExpected)]; 439 } 440 441 if (nodeToCheck.flags & NodeFlags.Ambient) { 442 return [createDiagnosticForNode(nodeToCheck, Messages.cannotExtractAmbientBlock)]; 443 } 444 445 // If we're in a class, see whether we're in a static region (static property initializer, static method, class constructor parameter default) 446 const containingClass = getContainingClass(nodeToCheck); 447 if (containingClass) { 448 checkForStaticContext(nodeToCheck, containingClass); 449 } 450 451 let errors: Diagnostic[] | undefined; 452 let permittedJumps = PermittedJumps.Return; 453 let seenLabels: __String[]; 454 455 visit(nodeToCheck); 456 457 if (rangeFacts & RangeFacts.UsesThis) { 458 const container = getThisContainer(nodeToCheck, /** includeArrowFunctions */ false); 459 if ( 460 container.kind === SyntaxKind.FunctionDeclaration || 461 (container.kind === SyntaxKind.MethodDeclaration && container.parent.kind === SyntaxKind.ObjectLiteralExpression) || 462 container.kind === SyntaxKind.FunctionExpression 463 ) { 464 rangeFacts |= RangeFacts.UsesThisInFunction; 465 } 466 } 467 468 return errors; 469 470 function visit(node: Node) { 471 if (errors) { 472 // already found an error - can stop now 473 return true; 474 } 475 476 if (isDeclaration(node)) { 477 const declaringNode = (node.kind === SyntaxKind.VariableDeclaration) ? node.parent.parent : node; 478 if (hasSyntacticModifier(declaringNode, ModifierFlags.Export)) { 479 // TODO: GH#18217 Silly to use `errors ||` since it's definitely not defined (see top of `visit`) 480 // Also, if we're only pushing one error, just use `let error: Diagnostic | undefined`! 481 // Also TODO: GH#19956 482 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); 483 return true; 484 } 485 } 486 487 // Some things can't be extracted in certain situations 488 switch (node.kind) { 489 case SyntaxKind.ImportDeclaration: 490 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractImport)); 491 return true; 492 case SyntaxKind.ExportAssignment: 493 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); 494 return true; 495 case SyntaxKind.SuperKeyword: 496 // For a super *constructor call*, we have to be extracting the entire class, 497 // but a super *method call* simply implies a 'this' reference 498 if (node.parent.kind === SyntaxKind.CallExpression) { 499 // Super constructor call 500 const containingClass = getContainingClass(node); 501 if (containingClass === undefined || containingClass.pos < span.start || containingClass.end >= (span.start + span.length)) { 502 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractSuper)); 503 return true; 504 } 505 } 506 else { 507 rangeFacts |= RangeFacts.UsesThis; 508 thisNode = node; 509 } 510 break; 511 case SyntaxKind.ArrowFunction: 512 // check if arrow function uses this 513 forEachChild(node, function check(n) { 514 if (isThis(n)) { 515 rangeFacts |= RangeFacts.UsesThis; 516 thisNode = node; 517 } 518 else if (isClassLike(n) || (isFunctionLike(n) && !isArrowFunction(n))) { 519 return false; 520 } 521 else { 522 forEachChild(n, check); 523 } 524 }); 525 // falls through 526 case SyntaxKind.ClassDeclaration: 527 case SyntaxKind.FunctionDeclaration: 528 if (isSourceFile(node.parent) && node.parent.externalModuleIndicator === undefined) { 529 // You cannot extract global declarations 530 (errors ||= []).push(createDiagnosticForNode(node, Messages.functionWillNotBeVisibleInTheNewScope)); 531 } 532 // falls through 533 case SyntaxKind.ClassExpression: 534 case SyntaxKind.FunctionExpression: 535 case SyntaxKind.MethodDeclaration: 536 case SyntaxKind.Constructor: 537 case SyntaxKind.GetAccessor: 538 case SyntaxKind.SetAccessor: 539 // do not dive into functions or classes 540 return false; 541 } 542 543 const savedPermittedJumps = permittedJumps; 544 switch (node.kind) { 545 case SyntaxKind.IfStatement: 546 permittedJumps &= ~PermittedJumps.Return; 547 break; 548 case SyntaxKind.TryStatement: 549 // forbid all jumps inside try blocks 550 permittedJumps = PermittedJumps.None; 551 break; 552 case SyntaxKind.Block: 553 if (node.parent && node.parent.kind === SyntaxKind.TryStatement && (node.parent as TryStatement).finallyBlock === node) { 554 // allow unconditional returns from finally blocks 555 permittedJumps = PermittedJumps.Return; 556 } 557 break; 558 case SyntaxKind.DefaultClause: 559 case SyntaxKind.CaseClause: 560 // allow unlabeled break inside case clauses 561 permittedJumps |= PermittedJumps.Break; 562 break; 563 default: 564 if (isIterationStatement(node, /*lookInLabeledStatements*/ false)) { 565 // allow unlabeled break/continue inside loops 566 permittedJumps |= PermittedJumps.Break | PermittedJumps.Continue; 567 } 568 break; 569 } 570 571 switch (node.kind) { 572 case SyntaxKind.ThisType: 573 case SyntaxKind.ThisKeyword: 574 rangeFacts |= RangeFacts.UsesThis; 575 thisNode = node; 576 break; 577 case SyntaxKind.LabeledStatement: { 578 const label = (node as LabeledStatement).label; 579 (seenLabels || (seenLabels = [])).push(label.escapedText); 580 forEachChild(node, visit); 581 seenLabels.pop(); 582 break; 583 } 584 case SyntaxKind.BreakStatement: 585 case SyntaxKind.ContinueStatement: { 586 const label = (node as BreakStatement | ContinueStatement).label; 587 if (label) { 588 if (!contains(seenLabels, label.escapedText)) { 589 // attempts to jump to label that is not in range to be extracted 590 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange)); 591 } 592 } 593 else { 594 if (!(permittedJumps & (node.kind === SyntaxKind.BreakStatement ? PermittedJumps.Break : PermittedJumps.Continue))) { 595 // attempt to break or continue in a forbidden context 596 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements)); 597 } 598 } 599 break; 600 } 601 case SyntaxKind.AwaitExpression: 602 rangeFacts |= RangeFacts.IsAsyncFunction; 603 break; 604 case SyntaxKind.YieldExpression: 605 rangeFacts |= RangeFacts.IsGenerator; 606 break; 607 case SyntaxKind.ReturnStatement: 608 if (permittedJumps & PermittedJumps.Return) { 609 rangeFacts |= RangeFacts.HasReturn; 610 } 611 else { 612 (errors ||= []).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalReturnStatement)); 613 } 614 break; 615 default: 616 forEachChild(node, visit); 617 break; 618 } 619 620 permittedJumps = savedPermittedJumps; 621 } 622 } 623 } 624 625 /** 626 * Includes the final semicolon so that the span covers statements in cases where it would otherwise 627 * only cover the declaration list. 628 */ 629 function getAdjustedSpanFromNodes(startNode: Node, endNode: Node, sourceFile: SourceFile): TextSpan { 630 const start = startNode.getStart(sourceFile); 631 let end = endNode.getEnd(); 632 if (sourceFile.text.charCodeAt(end) === CharacterCodes.semicolon) { 633 end++; 634 } 635 return { start, length: end - start }; 636 } 637 638 function getStatementOrExpressionRange(node: Node): Statement[] | Expression | undefined { 639 if (isStatement(node)) { 640 return [node]; 641 } 642 if (isExpressionNode(node)) { 643 // If our selection is the expression in an ExpressionStatement, expand 644 // the selection to include the enclosing Statement (this stops us 645 // from trying to care about the return value of the extracted function 646 // and eliminates double semicolon insertion in certain scenarios) 647 return isExpressionStatement(node.parent) ? [node.parent] : node as Expression; 648 } 649 if (isStringLiteralJsxAttribute(node)) { 650 return node; 651 } 652 return undefined; 653 } 654 655 function isScope(node: Node): node is Scope { 656 return isArrowFunction(node) ? isFunctionBody(node.body) : 657 isFunctionLikeDeclaration(node) || isSourceFile(node) || isModuleBlock(node) || isClassLike(node); 658 } 659 660 /** 661 * Computes possible places we could extract the function into. For example, 662 * you may be able to extract into a class method *or* local closure *or* namespace function, 663 * depending on what's in the extracted body. 664 */ 665 function collectEnclosingScopes(range: TargetRange): Scope[] { 666 let current: Node = isReadonlyArray(range.range) ? first(range.range) : range.range; 667 if (range.facts & RangeFacts.UsesThis && !(range.facts & RangeFacts.UsesThisInFunction)) { 668 // if range uses this as keyword or as type inside the class then it can only be extracted to a method of the containing class 669 const containingClass = getContainingClass(current); 670 if (containingClass) { 671 const containingFunction = findAncestor(current, isFunctionLikeDeclaration); 672 return containingFunction 673 ? [containingFunction, containingClass] 674 : [containingClass]; 675 } 676 } 677 678 const scopes: Scope[] = []; 679 while (true) { 680 current = current.parent; 681 // A function parameter's initializer is actually in the outer scope, not the function declaration 682 if (current.kind === SyntaxKind.Parameter) { 683 // Skip all the way to the outer scope of the function that declared this parameter 684 current = findAncestor(current, parent => isFunctionLikeDeclaration(parent))!.parent; 685 } 686 687 // We want to find the nearest parent where we can place an "equivalent" sibling to the node we're extracting out of. 688 // Walk up to the closest parent of a place where we can logically put a sibling: 689 // * Function declaration 690 // * Class declaration or expression 691 // * Module/namespace or source file 692 if (isScope(current)) { 693 scopes.push(current); 694 if (current.kind === SyntaxKind.SourceFile) { 695 return scopes; 696 } 697 } 698 } 699 } 700 701 function getFunctionExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo { 702 const { scopes, readsAndWrites: { target, usagesPerScope, functionErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); 703 Debug.assert(!functionErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); 704 context.cancellationToken!.throwIfCancellationRequested(); // TODO: GH#18217 705 return extractFunctionInScope(target, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], exposedVariableDeclarations, targetRange, context); 706 } 707 708 function getConstantExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo { 709 const { scopes, readsAndWrites: { target, usagesPerScope, constantErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); 710 Debug.assert(!constantErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); 711 Debug.assert(exposedVariableDeclarations.length === 0, "Extract constant accepted a range containing a variable declaration?"); 712 context.cancellationToken!.throwIfCancellationRequested(); 713 const expression = isExpression(target) 714 ? target 715 : (target.statements[0] as ExpressionStatement).expression; 716 return extractConstantInScope(expression, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange.facts, context); 717 } 718 719 interface Extraction { 720 readonly description: string; 721 readonly errors: readonly Diagnostic[]; 722 } 723 724 interface ScopeExtractions { 725 readonly functionExtraction: Extraction; 726 readonly constantExtraction: Extraction; 727 } 728 729 /** 730 * Given a piece of text to extract ('targetRange'), computes a list of possible extractions. 731 * Each returned ExtractResultForScope corresponds to a possible target scope and is either a set of changes 732 * or an error explaining why we can't extract into that scope. 733 */ 734 function getPossibleExtractions(targetRange: TargetRange, context: RefactorContext): readonly ScopeExtractions[] | undefined { 735 const { scopes, readsAndWrites: { functionErrorsPerScope, constantErrorsPerScope } } = getPossibleExtractionsWorker(targetRange, context); 736 // Need the inner type annotation to avoid https://github.com/Microsoft/TypeScript/issues/7547 737 const extractions = scopes.map((scope, i): ScopeExtractions => { 738 const functionDescriptionPart = getDescriptionForFunctionInScope(scope); 739 const constantDescriptionPart = getDescriptionForConstantInScope(scope); 740 741 const scopeDescription = isFunctionLikeDeclaration(scope) 742 ? getDescriptionForFunctionLikeDeclaration(scope) 743 : isClassLike(scope) 744 ? getDescriptionForClassLikeDeclaration(scope) 745 : getDescriptionForModuleLikeDeclaration(scope); 746 747 let functionDescription: string; 748 let constantDescription: string; 749 if (scopeDescription === SpecialScope.Global) { 750 functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "global"]); 751 constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "global"]); 752 } 753 else if (scopeDescription === SpecialScope.Module) { 754 functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "module"]); 755 constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "module"]); 756 } 757 else { 758 functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1), [functionDescriptionPart, scopeDescription]); 759 constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1), [constantDescriptionPart, scopeDescription]); 760 } 761 762 // Customize the phrasing for the innermost scope to increase clarity. 763 if (i === 0 && !isClassLike(scope)) { 764 constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_enclosing_scope), [constantDescriptionPart]); 765 } 766 767 return { 768 functionExtraction: { 769 description: functionDescription, 770 errors: functionErrorsPerScope[i], 771 }, 772 constantExtraction: { 773 description: constantDescription, 774 errors: constantErrorsPerScope[i], 775 }, 776 }; 777 }); 778 return extractions; 779 } 780 781 function getPossibleExtractionsWorker(targetRange: TargetRange, context: RefactorContext): { readonly scopes: Scope[], readonly readsAndWrites: ReadsAndWrites } { 782 const { file: sourceFile } = context; 783 784 const scopes = collectEnclosingScopes(targetRange); 785 const enclosingTextRange = getEnclosingTextRange(targetRange, sourceFile); 786 const readsAndWrites = collectReadsAndWrites( 787 targetRange, 788 scopes, 789 enclosingTextRange, 790 sourceFile, 791 context.program.getTypeChecker(), 792 context.cancellationToken!); 793 return { scopes, readsAndWrites }; 794 } 795 796 function getDescriptionForFunctionInScope(scope: Scope): string { 797 return isFunctionLikeDeclaration(scope) 798 ? "inner function" 799 : isClassLike(scope) 800 ? "method" 801 : "function"; 802 } 803 function getDescriptionForConstantInScope(scope: Scope): string { 804 return isClassLike(scope) 805 ? "readonly field" 806 : "constant"; 807 } 808 function getDescriptionForFunctionLikeDeclaration(scope: FunctionLikeDeclaration): string { 809 switch (scope.kind) { 810 case SyntaxKind.Constructor: 811 return "constructor"; 812 case SyntaxKind.FunctionExpression: 813 case SyntaxKind.FunctionDeclaration: 814 return scope.name 815 ? `function '${scope.name.text}'` 816 : ANONYMOUS; 817 case SyntaxKind.ArrowFunction: 818 return "arrow function"; 819 case SyntaxKind.MethodDeclaration: 820 return `method '${scope.name.getText()}'`; 821 case SyntaxKind.GetAccessor: 822 return `'get ${scope.name.getText()}'`; 823 case SyntaxKind.SetAccessor: 824 return `'set ${scope.name.getText()}'`; 825 default: 826 throw Debug.assertNever(scope, `Unexpected scope kind ${(scope as FunctionLikeDeclaration).kind}`); 827 } 828 } 829 function getDescriptionForClassLikeDeclaration(scope: ClassLikeDeclaration): string { 830 return scope.kind === SyntaxKind.ClassDeclaration 831 ? scope.name ? `class '${scope.name.text}'` : "anonymous class declaration" 832 : scope.name ? `class expression '${scope.name.text}'` : "anonymous class expression"; 833 } 834 function getDescriptionForModuleLikeDeclaration(scope: SourceFile | ModuleBlock): string | SpecialScope { 835 return scope.kind === SyntaxKind.ModuleBlock 836 ? `namespace '${scope.parent.name.getText()}'` 837 : scope.externalModuleIndicator ? SpecialScope.Module : SpecialScope.Global; 838 } 839 840 const enum SpecialScope { 841 Module, 842 Global, 843 } 844 845 /** 846 * Result of 'extractRange' operation for a specific scope. 847 * Stores either a list of changes that should be applied to extract a range or a list of errors 848 */ 849 function extractFunctionInScope( 850 node: Statement | Expression | Block, 851 scope: Scope, 852 { usages: usagesInScope, typeParameterUsages, substitutions }: ScopeUsages, 853 exposedVariableDeclarations: readonly VariableDeclaration[], 854 range: TargetRange, 855 context: RefactorContext): RefactorEditInfo { 856 857 const checker = context.program.getTypeChecker(); 858 const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); 859 const importAdder = codefix.createImportAdder(context.file, context.program, context.preferences, context.host); 860 861 // Make a unique name for the extracted function 862 const file = scope.getSourceFile(); 863 const functionNameText = getUniqueName(isClassLike(scope) ? "newMethod" : "newFunction", file); 864 const isJS = isInJSFile(scope); 865 866 const functionName = factory.createIdentifier(functionNameText); 867 868 let returnType: TypeNode | undefined; 869 const parameters: ParameterDeclaration[] = []; 870 const callArguments: Identifier[] = []; 871 let writes: UsageEntry[] | undefined; 872 usagesInScope.forEach((usage, name) => { 873 let typeNode: TypeNode | undefined; 874 if (!isJS) { 875 let type = checker.getTypeOfSymbolAtLocation(usage.symbol, usage.node); 876 // Widen the type so we don't emit nonsense annotations like "function fn(x: 3) {" 877 type = checker.getBaseTypeOfLiteralType(type); 878 typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, scope, scriptTarget, NodeBuilderFlags.NoTruncation); 879 } 880 881 const paramDecl = factory.createParameterDeclaration( 882 /*modifiers*/ undefined, 883 /*dotDotDotToken*/ undefined, 884 /*name*/ name, 885 /*questionToken*/ undefined, 886 typeNode 887 ); 888 parameters.push(paramDecl); 889 if (usage.usage === Usage.Write) { 890 (writes || (writes = [])).push(usage); 891 } 892 callArguments.push(factory.createIdentifier(name)); 893 }); 894 895 const typeParametersAndDeclarations = arrayFrom(typeParameterUsages.values()).map(type => ({ type, declaration: getFirstDeclaration(type) })); 896 const sortedTypeParametersAndDeclarations = typeParametersAndDeclarations.sort(compareTypesByDeclarationOrder); 897 898 const typeParameters: readonly TypeParameterDeclaration[] | undefined = sortedTypeParametersAndDeclarations.length === 0 899 ? undefined 900 : sortedTypeParametersAndDeclarations.map(t => t.declaration as TypeParameterDeclaration); 901 902 // Strictly speaking, we should check whether each name actually binds to the appropriate type 903 // parameter. In cases of shadowing, they may not. 904 const callTypeArguments: readonly TypeNode[] | undefined = typeParameters !== undefined 905 ? typeParameters.map(decl => factory.createTypeReferenceNode(decl.name, /*typeArguments*/ undefined)) 906 : undefined; 907 908 // Provide explicit return types for contextually-typed functions 909 // to avoid problems when there are literal types present 910 if (isExpression(node) && !isJS) { 911 const contextualType = checker.getContextualType(node); 912 returnType = checker.typeToTypeNode(contextualType!, scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217 913 } 914 915 const { body, returnValueProperty } = transformFunctionBody(node, exposedVariableDeclarations, writes, substitutions, !!(range.facts & RangeFacts.HasReturn)); 916 suppressLeadingAndTrailingTrivia(body); 917 918 let newFunction: MethodDeclaration | FunctionDeclaration; 919 920 const callThis = !!(range.facts & RangeFacts.UsesThisInFunction); 921 922 if (isClassLike(scope)) { 923 // always create private method in TypeScript files 924 const modifiers: Modifier[] = isJS ? [] : [factory.createModifier(SyntaxKind.PrivateKeyword)]; 925 if (range.facts & RangeFacts.InStaticRegion) { 926 modifiers.push(factory.createModifier(SyntaxKind.StaticKeyword)); 927 } 928 if (range.facts & RangeFacts.IsAsyncFunction) { 929 modifiers.push(factory.createModifier(SyntaxKind.AsyncKeyword)); 930 } 931 newFunction = factory.createMethodDeclaration( 932 modifiers.length ? modifiers : undefined, 933 range.facts & RangeFacts.IsGenerator ? factory.createToken(SyntaxKind.AsteriskToken) : undefined, 934 functionName, 935 /*questionToken*/ undefined, 936 typeParameters, 937 parameters, 938 returnType, 939 body 940 ); 941 } 942 else { 943 if (callThis) { 944 parameters.unshift( 945 factory.createParameterDeclaration( 946 /*modifiers*/ undefined, 947 /*dotDotDotToken*/ undefined, 948 /*name*/ "this", 949 /*questionToken*/ undefined, 950 checker.typeToTypeNode( 951 checker.getTypeAtLocation(range.thisNode!), 952 scope, 953 NodeBuilderFlags.NoTruncation 954 ), 955 /*initializer*/ undefined, 956 ) 957 ); 958 } 959 newFunction = factory.createFunctionDeclaration( 960 range.facts & RangeFacts.IsAsyncFunction ? [factory.createToken(SyntaxKind.AsyncKeyword)] : undefined, 961 range.facts & RangeFacts.IsGenerator ? factory.createToken(SyntaxKind.AsteriskToken) : undefined, 962 functionName, 963 typeParameters, 964 parameters, 965 returnType, 966 body 967 ); 968 } 969 970 const changeTracker = textChanges.ChangeTracker.fromContext(context); 971 const minInsertionPos = (isReadonlyArray(range.range) ? last(range.range) : range.range).end; 972 const nodeToInsertBefore = getNodeToInsertFunctionBefore(minInsertionPos, scope); 973 if (nodeToInsertBefore) { 974 changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newFunction, /*blankLineBetween*/ true); 975 } 976 else { 977 changeTracker.insertNodeAtEndOfScope(context.file, scope, newFunction); 978 } 979 importAdder.writeFixes(changeTracker); 980 981 const newNodes: Node[] = []; 982 // replace range with function call 983 const called = getCalledExpression(scope, range, functionNameText); 984 985 if (callThis) { 986 callArguments.unshift(factory.createIdentifier("this")); 987 } 988 989 let call: Expression = factory.createCallExpression( 990 callThis ? factory.createPropertyAccessExpression( 991 called, 992 "call" 993 ) : called, 994 callTypeArguments, // Note that no attempt is made to take advantage of type argument inference 995 callArguments); 996 if (range.facts & RangeFacts.IsGenerator) { 997 call = factory.createYieldExpression(factory.createToken(SyntaxKind.AsteriskToken), call); 998 } 999 if (range.facts & RangeFacts.IsAsyncFunction) { 1000 call = factory.createAwaitExpression(call); 1001 } 1002 if (isInJSXContent(node)) { 1003 call = factory.createJsxExpression(/*dotDotDotToken*/ undefined, call); 1004 } 1005 1006 if (exposedVariableDeclarations.length && !writes) { 1007 // No need to mix declarations and writes. 1008 1009 // How could any variables be exposed if there's a return statement? 1010 Debug.assert(!returnValueProperty, "Expected no returnValueProperty"); 1011 Debug.assert(!(range.facts & RangeFacts.HasReturn), "Expected RangeFacts.HasReturn flag to be unset"); 1012 1013 if (exposedVariableDeclarations.length === 1) { 1014 // Declaring exactly one variable: let x = newFunction(); 1015 const variableDeclaration = exposedVariableDeclarations[0]; 1016 newNodes.push(factory.createVariableStatement( 1017 /*modifiers*/ undefined, 1018 factory.createVariableDeclarationList( 1019 [factory.createVariableDeclaration(getSynthesizedDeepClone(variableDeclaration.name), /*exclamationToken*/ undefined, /*type*/ getSynthesizedDeepClone(variableDeclaration.type), /*initializer*/ call)], // TODO (acasey): test binding patterns 1020 variableDeclaration.parent.flags))); 1021 } 1022 else { 1023 // Declaring multiple variables / return properties: 1024 // let {x, y} = newFunction(); 1025 const bindingElements: BindingElement[] = []; 1026 const typeElements: TypeElement[] = []; 1027 let commonNodeFlags = exposedVariableDeclarations[0].parent.flags; 1028 let sawExplicitType = false; 1029 for (const variableDeclaration of exposedVariableDeclarations) { 1030 bindingElements.push(factory.createBindingElement( 1031 /*dotDotDotToken*/ undefined, 1032 /*propertyName*/ undefined, 1033 /*name*/ getSynthesizedDeepClone(variableDeclaration.name))); 1034 1035 // Being returned through an object literal will have widened the type. 1036 const variableType: TypeNode | undefined = checker.typeToTypeNode( 1037 checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(variableDeclaration)), 1038 scope, 1039 NodeBuilderFlags.NoTruncation); 1040 1041 typeElements.push(factory.createPropertySignature( 1042 /*modifiers*/ undefined, 1043 /*name*/ variableDeclaration.symbol.name, 1044 /*questionToken*/ undefined, 1045 /*type*/ variableType)); 1046 sawExplicitType = sawExplicitType || variableDeclaration.type !== undefined; 1047 commonNodeFlags = commonNodeFlags & variableDeclaration.parent.flags; 1048 } 1049 1050 const typeLiteral: TypeLiteralNode | undefined = sawExplicitType ? factory.createTypeLiteralNode(typeElements) : undefined; 1051 if (typeLiteral) { 1052 setEmitFlags(typeLiteral, EmitFlags.SingleLine); 1053 } 1054 1055 newNodes.push(factory.createVariableStatement( 1056 /*modifiers*/ undefined, 1057 factory.createVariableDeclarationList( 1058 [factory.createVariableDeclaration( 1059 factory.createObjectBindingPattern(bindingElements), 1060 /*exclamationToken*/ undefined, 1061 /*type*/ typeLiteral, 1062 /*initializer*/call)], 1063 commonNodeFlags))); 1064 } 1065 } 1066 else if (exposedVariableDeclarations.length || writes) { 1067 if (exposedVariableDeclarations.length) { 1068 // CONSIDER: we're going to create one statement per variable, but we could actually preserve their original grouping. 1069 for (const variableDeclaration of exposedVariableDeclarations) { 1070 let flags: NodeFlags = variableDeclaration.parent.flags; 1071 if (flags & NodeFlags.Const) { 1072 flags = (flags & ~NodeFlags.Const) | NodeFlags.Let; 1073 } 1074 1075 newNodes.push(factory.createVariableStatement( 1076 /*modifiers*/ undefined, 1077 factory.createVariableDeclarationList( 1078 [factory.createVariableDeclaration(variableDeclaration.symbol.name, /*exclamationToken*/ undefined, getTypeDeepCloneUnionUndefined(variableDeclaration.type))], 1079 flags))); 1080 } 1081 } 1082 1083 if (returnValueProperty) { 1084 // has both writes and return, need to create variable declaration to hold return value; 1085 newNodes.push(factory.createVariableStatement( 1086 /*modifiers*/ undefined, 1087 factory.createVariableDeclarationList( 1088 [factory.createVariableDeclaration(returnValueProperty, /*exclamationToken*/ undefined, getTypeDeepCloneUnionUndefined(returnType))], 1089 NodeFlags.Let))); 1090 } 1091 1092 const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); 1093 if (returnValueProperty) { 1094 assignments.unshift(factory.createShorthandPropertyAssignment(returnValueProperty)); 1095 } 1096 1097 // propagate writes back 1098 if (assignments.length === 1) { 1099 // We would only have introduced a return value property if there had been 1100 // other assignments to make. 1101 Debug.assert(!returnValueProperty, "Shouldn't have returnValueProperty here"); 1102 1103 newNodes.push(factory.createExpressionStatement(factory.createAssignment(assignments[0].name, call))); 1104 1105 if (range.facts & RangeFacts.HasReturn) { 1106 newNodes.push(factory.createReturnStatement()); 1107 } 1108 } 1109 else { 1110 // emit e.g. 1111 // { a, b, __return } = newFunction(a, b); 1112 // return __return; 1113 newNodes.push(factory.createExpressionStatement(factory.createAssignment(factory.createObjectLiteralExpression(assignments), call))); 1114 if (returnValueProperty) { 1115 newNodes.push(factory.createReturnStatement(factory.createIdentifier(returnValueProperty))); 1116 } 1117 } 1118 } 1119 else { 1120 if (range.facts & RangeFacts.HasReturn) { 1121 newNodes.push(factory.createReturnStatement(call)); 1122 } 1123 else if (isReadonlyArray(range.range)) { 1124 newNodes.push(factory.createExpressionStatement(call)); 1125 } 1126 else { 1127 newNodes.push(call); 1128 } 1129 } 1130 1131 if (isReadonlyArray(range.range)) { 1132 changeTracker.replaceNodeRangeWithNodes(context.file, first(range.range), last(range.range), newNodes); 1133 } 1134 else { 1135 changeTracker.replaceNodeWithNodes(context.file, range.range, newNodes); 1136 } 1137 1138 const edits = changeTracker.getChanges(); 1139 const renameRange = isReadonlyArray(range.range) ? first(range.range) : range.range; 1140 1141 const renameFilename = renameRange.getSourceFile().fileName; 1142 const renameLocation = getRenameLocation(edits, renameFilename, functionNameText, /*isDeclaredBeforeUse*/ false); 1143 return { renameFilename, renameLocation, edits }; 1144 1145 function getTypeDeepCloneUnionUndefined(typeNode: TypeNode | undefined): TypeNode | undefined { 1146 if (typeNode === undefined) { 1147 return undefined; 1148 } 1149 1150 const clone = getSynthesizedDeepClone(typeNode); 1151 let withoutParens = clone; 1152 while (isParenthesizedTypeNode(withoutParens)) { 1153 withoutParens = withoutParens.type; 1154 } 1155 return isUnionTypeNode(withoutParens) && find(withoutParens.types, t => t.kind === SyntaxKind.UndefinedKeyword) 1156 ? clone 1157 : factory.createUnionTypeNode([clone, factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword)]); 1158 } 1159 } 1160 1161 /** 1162 * Result of 'extractRange' operation for a specific scope. 1163 * Stores either a list of changes that should be applied to extract a range or a list of errors 1164 */ 1165 function extractConstantInScope( 1166 node: Expression, 1167 scope: Scope, 1168 { substitutions }: ScopeUsages, 1169 rangeFacts: RangeFacts, 1170 context: RefactorContext): RefactorEditInfo { 1171 1172 const checker = context.program.getTypeChecker(); 1173 1174 // Make a unique name for the extracted variable 1175 const file = scope.getSourceFile(); 1176 const localNameText = isPropertyAccessExpression(node) && !isClassLike(scope) && !checker.resolveName(node.name.text, node, SymbolFlags.Value, /*excludeGlobals*/ false) && !isPrivateIdentifier(node.name) && !isKeyword(node.name.originalKeywordKind!) 1177 ? node.name.text 1178 : getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file); 1179 const isJS = isInJSFile(scope); 1180 1181 let variableType = isJS || !checker.isContextSensitive(node) 1182 ? undefined 1183 : checker.typeToTypeNode(checker.getContextualType(node)!, scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217 1184 1185 let initializer = transformConstantInitializer(skipParentheses(node), substitutions); 1186 1187 ({ variableType, initializer } = transformFunctionInitializerAndType(variableType, initializer)); 1188 1189 suppressLeadingAndTrailingTrivia(initializer); 1190 1191 const changeTracker = textChanges.ChangeTracker.fromContext(context); 1192 1193 if (isClassLike(scope)) { 1194 Debug.assert(!isJS, "Cannot extract to a JS class"); // See CannotExtractToJSClass 1195 const modifiers: Modifier[] = []; 1196 modifiers.push(factory.createModifier(SyntaxKind.PrivateKeyword)); 1197 if (rangeFacts & RangeFacts.InStaticRegion) { 1198 modifiers.push(factory.createModifier(SyntaxKind.StaticKeyword)); 1199 } 1200 modifiers.push(factory.createModifier(SyntaxKind.ReadonlyKeyword)); 1201 1202 const newVariable = factory.createPropertyDeclaration( 1203 modifiers, 1204 localNameText, 1205 /*questionToken*/ undefined, 1206 variableType, 1207 initializer); 1208 1209 let localReference: Expression = factory.createPropertyAccessExpression( 1210 rangeFacts & RangeFacts.InStaticRegion 1211 ? factory.createIdentifier(scope.name!.getText()) // TODO: GH#18217 1212 : factory.createThis(), 1213 factory.createIdentifier(localNameText)); 1214 1215 if (isInJSXContent(node)) { 1216 localReference = factory.createJsxExpression(/*dotDotDotToken*/ undefined, localReference); 1217 } 1218 1219 // Declare 1220 const maxInsertionPos = node.pos; 1221 const nodeToInsertBefore = getNodeToInsertPropertyBefore(maxInsertionPos, scope); 1222 changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, /*blankLineBetween*/ true); 1223 1224 // Consume 1225 changeTracker.replaceNode(context.file, node, localReference); 1226 } 1227 else { 1228 const newVariableDeclaration = factory.createVariableDeclaration(localNameText, /*exclamationToken*/ undefined, variableType, initializer); 1229 1230 // If the node is part of an initializer in a list of variable declarations, insert a new 1231 // variable declaration into the list (in case it depends on earlier ones). 1232 // CONSIDER: If the declaration list isn't const, we might want to split it into multiple 1233 // lists so that the newly extracted one can be const. 1234 const oldVariableDeclaration = getContainingVariableDeclarationIfInList(node, scope); 1235 if (oldVariableDeclaration) { 1236 // Declare 1237 // CONSIDER: could detect that each is on a separate line (See `extractConstant_VariableList_MultipleLines` in `extractConstants.ts`) 1238 changeTracker.insertNodeBefore(context.file, oldVariableDeclaration, newVariableDeclaration); 1239 1240 // Consume 1241 const localReference = factory.createIdentifier(localNameText); 1242 changeTracker.replaceNode(context.file, node, localReference); 1243 } 1244 else if (node.parent.kind === SyntaxKind.ExpressionStatement && scope === findAncestor(node, isScope)) { 1245 // If the parent is an expression statement and the target scope is the immediately enclosing one, 1246 // replace the statement with the declaration. 1247 const newVariableStatement = factory.createVariableStatement( 1248 /*modifiers*/ undefined, 1249 factory.createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const)); 1250 changeTracker.replaceNode(context.file, node.parent, newVariableStatement); 1251 } 1252 else { 1253 const newVariableStatement = factory.createVariableStatement( 1254 /*modifiers*/ undefined, 1255 factory.createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const)); 1256 1257 // Declare 1258 const nodeToInsertBefore = getNodeToInsertConstantBefore(node, scope); 1259 if (nodeToInsertBefore.pos === 0) { 1260 changeTracker.insertNodeAtTopOfFile(context.file, newVariableStatement, /*blankLineBetween*/ false); 1261 } 1262 else { 1263 changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariableStatement, /*blankLineBetween*/ false); 1264 } 1265 1266 // Consume 1267 if (node.parent.kind === SyntaxKind.ExpressionStatement) { 1268 // If the parent is an expression statement, delete it. 1269 changeTracker.delete(context.file, node.parent); 1270 } 1271 else { 1272 let localReference: Expression = factory.createIdentifier(localNameText); 1273 // When extract to a new variable in JSX content, need to wrap a {} out of the new variable 1274 // or it will become a plain text 1275 if (isInJSXContent(node)) { 1276 localReference = factory.createJsxExpression(/*dotDotDotToken*/ undefined, localReference); 1277 } 1278 changeTracker.replaceNode(context.file, node, localReference); 1279 } 1280 } 1281 } 1282 1283 const edits = changeTracker.getChanges(); 1284 1285 const renameFilename = node.getSourceFile().fileName; 1286 const renameLocation = getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true); 1287 return { renameFilename, renameLocation, edits }; 1288 1289 function transformFunctionInitializerAndType(variableType: TypeNode | undefined, initializer: Expression): { variableType: TypeNode | undefined, initializer: Expression } { 1290 // If no contextual type exists there is nothing to transfer to the function signature 1291 if (variableType === undefined) return { variableType, initializer }; 1292 // Only do this for function expressions and arrow functions that are not generic 1293 if (!isFunctionExpression(initializer) && !isArrowFunction(initializer) || !!initializer.typeParameters) return { variableType, initializer }; 1294 const functionType = checker.getTypeAtLocation(node); 1295 const functionSignature = singleOrUndefined(checker.getSignaturesOfType(functionType, SignatureKind.Call)); 1296 1297 // If no function signature, maybe there was an error, do nothing 1298 if (!functionSignature) return { variableType, initializer }; 1299 // If the function signature has generic type parameters we don't attempt to move the parameters 1300 if (!!functionSignature.getTypeParameters()) return { variableType, initializer }; 1301 1302 // We add parameter types if needed 1303 const parameters: ParameterDeclaration[] = []; 1304 let hasAny = false; 1305 for (const p of initializer.parameters) { 1306 if (p.type) { 1307 parameters.push(p); 1308 } 1309 else { 1310 const paramType = checker.getTypeAtLocation(p); 1311 if (paramType === checker.getAnyType()) hasAny = true; 1312 1313 parameters.push(factory.updateParameterDeclaration(p, 1314 p.modifiers, p.dotDotDotToken, 1315 p.name, p.questionToken, p.type || checker.typeToTypeNode(paramType, scope, NodeBuilderFlags.NoTruncation), p.initializer)); 1316 } 1317 } 1318 // If a parameter was inferred as any we skip adding function parameters at all. 1319 // Turning an implicit any (which under common settings is a error) to an explicit 1320 // is probably actually a worse refactor outcome. 1321 if (hasAny) return { variableType, initializer }; 1322 variableType = undefined; 1323 if (isArrowFunction(initializer)) { 1324 initializer = factory.updateArrowFunction(initializer, canHaveModifiers(node) ? getModifiers(node) : undefined, initializer.typeParameters, 1325 parameters, 1326 initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation), 1327 initializer.equalsGreaterThanToken, 1328 initializer.body); 1329 } 1330 else { 1331 if (functionSignature && !!functionSignature.thisParameter) { 1332 const firstParameter = firstOrUndefined(parameters); 1333 // If the function signature has a this parameter and if the first defined parameter is not the this parameter, we must add it 1334 // Note: If this parameter was already there, it would have been previously updated with the type if not type was present 1335 if ((!firstParameter || (isIdentifier(firstParameter.name) && firstParameter.name.escapedText !== "this"))) { 1336 const thisType = checker.getTypeOfSymbolAtLocation(functionSignature.thisParameter, node); 1337 parameters.splice(0, 0, factory.createParameterDeclaration( 1338 /* modifiers */ undefined, 1339 /* dotDotDotToken */ undefined, 1340 "this", 1341 /* questionToken */ undefined, 1342 checker.typeToTypeNode(thisType, scope, NodeBuilderFlags.NoTruncation) 1343 )); 1344 } 1345 } 1346 initializer = factory.updateFunctionExpression(initializer, canHaveModifiers(node) ? getModifiers(node) : undefined, initializer.asteriskToken, 1347 initializer.name, initializer.typeParameters, 1348 parameters, 1349 initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation), 1350 initializer.body); 1351 } 1352 return { variableType, initializer }; 1353 } 1354 } 1355 1356 function getContainingVariableDeclarationIfInList(node: Node, scope: Scope) { 1357 let prevNode; 1358 while (node !== undefined && node !== scope) { 1359 if (isVariableDeclaration(node) && 1360 node.initializer === prevNode && 1361 isVariableDeclarationList(node.parent) && 1362 node.parent.declarations.length > 1) { 1363 1364 return node; 1365 } 1366 1367 prevNode = node; 1368 node = node.parent; 1369 } 1370 } 1371 1372 function getFirstDeclaration(type: Type): Declaration | undefined { 1373 let firstDeclaration; 1374 1375 const symbol = type.symbol; 1376 if (symbol && symbol.declarations) { 1377 for (const declaration of symbol.declarations) { 1378 if (firstDeclaration === undefined || declaration.pos < firstDeclaration.pos) { 1379 firstDeclaration = declaration; 1380 } 1381 } 1382 } 1383 1384 return firstDeclaration; 1385 } 1386 1387 function compareTypesByDeclarationOrder( 1388 { type: type1, declaration: declaration1 }: { type: Type, declaration?: Declaration }, 1389 { type: type2, declaration: declaration2 }: { type: Type, declaration?: Declaration }) { 1390 1391 return compareProperties(declaration1, declaration2, "pos", compareValues) 1392 || compareStringsCaseSensitive( 1393 type1.symbol ? type1.symbol.getName() : "", 1394 type2.symbol ? type2.symbol.getName() : "") 1395 || compareValues(type1.id, type2.id); 1396 } 1397 1398 function getCalledExpression(scope: Node, range: TargetRange, functionNameText: string): Expression { 1399 const functionReference = factory.createIdentifier(functionNameText); 1400 if (isClassLike(scope)) { 1401 const lhs = range.facts & RangeFacts.InStaticRegion ? factory.createIdentifier(scope.name!.text) : factory.createThis(); // TODO: GH#18217 1402 return factory.createPropertyAccessExpression(lhs, functionReference); 1403 } 1404 else { 1405 return functionReference; 1406 } 1407 } 1408 1409 function transformFunctionBody(body: Node, exposedVariableDeclarations: readonly VariableDeclaration[], writes: readonly UsageEntry[] | undefined, substitutions: ReadonlyESMap<string, Node>, hasReturn: boolean): { body: Block, returnValueProperty: string | undefined } { 1410 const hasWritesOrVariableDeclarations = writes !== undefined || exposedVariableDeclarations.length > 0; 1411 if (isBlock(body) && !hasWritesOrVariableDeclarations && substitutions.size === 0) { 1412 // already block, no declarations or writes to propagate back, no substitutions - can use node as is 1413 return { body: factory.createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined }; 1414 } 1415 let returnValueProperty: string | undefined; 1416 let ignoreReturns = false; 1417 const statements = factory.createNodeArray(isBlock(body) ? body.statements.slice(0) : [isStatement(body) ? body : factory.createReturnStatement(skipParentheses(body as Expression))]); 1418 // rewrite body if either there are writes that should be propagated back via return statements or there are substitutions 1419 if (hasWritesOrVariableDeclarations || substitutions.size) { 1420 const rewrittenStatements = visitNodes(statements, visitor).slice(); 1421 if (hasWritesOrVariableDeclarations && !hasReturn && isStatement(body)) { 1422 // add return at the end to propagate writes back in case if control flow falls out of the function body 1423 // it is ok to know that range has at least one return since it we only allow unconditional returns 1424 const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); 1425 if (assignments.length === 1) { 1426 rewrittenStatements.push(factory.createReturnStatement(assignments[0].name)); 1427 } 1428 else { 1429 rewrittenStatements.push(factory.createReturnStatement(factory.createObjectLiteralExpression(assignments))); 1430 } 1431 } 1432 return { body: factory.createBlock(rewrittenStatements, /*multiLine*/ true), returnValueProperty }; 1433 } 1434 else { 1435 return { body: factory.createBlock(statements, /*multiLine*/ true), returnValueProperty: undefined }; 1436 } 1437 1438 function visitor(node: Node): VisitResult<Node> { 1439 if (!ignoreReturns && isReturnStatement(node) && hasWritesOrVariableDeclarations) { 1440 const assignments: ObjectLiteralElementLike[] = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); 1441 if (node.expression) { 1442 if (!returnValueProperty) { 1443 returnValueProperty = "__return"; 1444 } 1445 assignments.unshift(factory.createPropertyAssignment(returnValueProperty, visitNode(node.expression, visitor))); 1446 } 1447 if (assignments.length === 1) { 1448 return factory.createReturnStatement(assignments[0].name as Expression); 1449 } 1450 else { 1451 return factory.createReturnStatement(factory.createObjectLiteralExpression(assignments)); 1452 } 1453 } 1454 else { 1455 const oldIgnoreReturns = ignoreReturns; 1456 ignoreReturns = ignoreReturns || isFunctionLikeDeclaration(node) || isClassLike(node); 1457 const substitution = substitutions.get(getNodeId(node).toString()); 1458 const result = substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); 1459 ignoreReturns = oldIgnoreReturns; 1460 return result; 1461 } 1462 } 1463 } 1464 1465 function transformConstantInitializer(initializer: Expression, substitutions: ReadonlyESMap<string, Node>): Expression { 1466 return substitutions.size 1467 ? visitor(initializer) as Expression 1468 : initializer; 1469 1470 function visitor(node: Node): VisitResult<Node> { 1471 const substitution = substitutions.get(getNodeId(node).toString()); 1472 return substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); 1473 } 1474 } 1475 1476 function getStatementsOrClassElements(scope: Scope): readonly Statement[] | readonly ClassElement[] { 1477 if (isFunctionLikeDeclaration(scope)) { 1478 const body = scope.body!; // TODO: GH#18217 1479 if (isBlock(body)) { 1480 return body.statements; 1481 } 1482 } 1483 else if (isModuleBlock(scope) || isSourceFile(scope)) { 1484 return scope.statements; 1485 } 1486 else if (isClassLike(scope)) { 1487 return scope.members; 1488 } 1489 else { 1490 assertType<never>(scope); 1491 } 1492 1493 return emptyArray; 1494 } 1495 1496 /** 1497 * If `scope` contains a function after `minPos`, then return the first such function. 1498 * Otherwise, return `undefined`. 1499 */ 1500 function getNodeToInsertFunctionBefore(minPos: number, scope: Scope): Statement | ClassElement | undefined { 1501 return find<Statement | ClassElement>(getStatementsOrClassElements(scope), child => 1502 child.pos >= minPos && isFunctionLikeDeclaration(child) && !isConstructorDeclaration(child)); 1503 } 1504 1505 function getNodeToInsertPropertyBefore(maxPos: number, scope: ClassLikeDeclaration): ClassElement { 1506 const members = scope.members; 1507 Debug.assert(members.length > 0, "Found no members"); // There must be at least one child, since we extracted from one. 1508 1509 let prevMember: ClassElement | undefined; 1510 let allProperties = true; 1511 for (const member of members) { 1512 if (member.pos > maxPos) { 1513 return prevMember || members[0]; 1514 } 1515 if (allProperties && !isPropertyDeclaration(member)) { 1516 // If it is non-vacuously true that all preceding members are properties, 1517 // insert before the current member (i.e. at the end of the list of properties). 1518 if (prevMember !== undefined) { 1519 return member; 1520 } 1521 1522 allProperties = false; 1523 } 1524 prevMember = member; 1525 } 1526 1527 if (prevMember === undefined) return Debug.fail(); // If the loop didn't return, then it did set prevMember. 1528 return prevMember; 1529 } 1530 1531 function getNodeToInsertConstantBefore(node: Node, scope: Scope): Statement { 1532 Debug.assert(!isClassLike(scope)); 1533 1534 let prevScope: Scope | undefined; 1535 for (let curr = node; curr !== scope; curr = curr.parent) { 1536 if (isScope(curr)) { 1537 prevScope = curr; 1538 } 1539 } 1540 1541 for (let curr = (prevScope || node).parent; ; curr = curr.parent) { 1542 if (isBlockLike(curr)) { 1543 let prevStatement: Statement | undefined; 1544 for (const statement of curr.statements) { 1545 if (statement.pos > node.pos) { 1546 break; 1547 } 1548 prevStatement = statement; 1549 } 1550 1551 if (!prevStatement && isCaseClause(curr)) { 1552 // We must have been in the expression of the case clause. 1553 Debug.assert(isSwitchStatement(curr.parent.parent), "Grandparent isn't a switch statement"); 1554 return curr.parent.parent; 1555 } 1556 1557 // There must be at least one statement since we started in one. 1558 return Debug.checkDefined(prevStatement, "prevStatement failed to get set"); 1559 } 1560 1561 Debug.assert(curr !== scope, "Didn't encounter a block-like before encountering scope"); 1562 } 1563 } 1564 1565 function getPropertyAssignmentsForWritesAndVariableDeclarations( 1566 exposedVariableDeclarations: readonly VariableDeclaration[], 1567 writes: readonly UsageEntry[] | undefined 1568 ): ShorthandPropertyAssignment[] { 1569 const variableAssignments = map(exposedVariableDeclarations, v => factory.createShorthandPropertyAssignment(v.symbol.name)); 1570 const writeAssignments = map(writes, w => factory.createShorthandPropertyAssignment(w.symbol.name)); 1571 1572 // TODO: GH#18217 `variableAssignments` not possibly undefined! 1573 return variableAssignments === undefined 1574 ? writeAssignments! 1575 : writeAssignments === undefined 1576 ? variableAssignments 1577 : variableAssignments.concat(writeAssignments); 1578 } 1579 1580 function isReadonlyArray(v: any): v is readonly any[] { 1581 return isArray(v); 1582 } 1583 1584 /** 1585 * Produces a range that spans the entirety of nodes, given a selection 1586 * that might start/end in the middle of nodes. 1587 * 1588 * For example, when the user makes a selection like this 1589 * v---v 1590 * var someThing = foo + bar; 1591 * this returns ^-------^ 1592 */ 1593 function getEnclosingTextRange(targetRange: TargetRange, sourceFile: SourceFile): TextRange { 1594 return isReadonlyArray(targetRange.range) 1595 ? { pos: first(targetRange.range).getStart(sourceFile), end: last(targetRange.range).getEnd() } 1596 : targetRange.range; 1597 } 1598 1599 const enum Usage { 1600 // value should be passed to extracted method 1601 Read = 1, 1602 // value should be passed to extracted method and propagated back 1603 Write = 2 1604 } 1605 1606 interface UsageEntry { 1607 readonly usage: Usage; 1608 readonly symbol: Symbol; 1609 readonly node: Node; 1610 } 1611 1612 interface ScopeUsages { 1613 readonly usages: ESMap<string, UsageEntry>; 1614 readonly typeParameterUsages: ESMap<string, TypeParameter>; // Key is type ID 1615 readonly substitutions: ESMap<string, Node>; 1616 } 1617 1618 interface ReadsAndWrites { 1619 readonly target: Expression | Block; 1620 readonly usagesPerScope: readonly ScopeUsages[]; 1621 readonly functionErrorsPerScope: readonly (readonly Diagnostic[])[]; 1622 readonly constantErrorsPerScope: readonly (readonly Diagnostic[])[]; 1623 readonly exposedVariableDeclarations: readonly VariableDeclaration[]; 1624 } 1625 function collectReadsAndWrites( 1626 targetRange: TargetRange, 1627 scopes: Scope[], 1628 enclosingTextRange: TextRange, 1629 sourceFile: SourceFile, 1630 checker: TypeChecker, 1631 cancellationToken: CancellationToken): ReadsAndWrites { 1632 1633 const allTypeParameterUsages = new Map<string, TypeParameter>(); // Key is type ID 1634 const usagesPerScope: ScopeUsages[] = []; 1635 const substitutionsPerScope: ESMap<string, Node>[] = []; 1636 const functionErrorsPerScope: Diagnostic[][] = []; 1637 const constantErrorsPerScope: Diagnostic[][] = []; 1638 const visibleDeclarationsInExtractedRange: NamedDeclaration[] = []; 1639 const exposedVariableSymbolSet = new Map<string, true>(); // Key is symbol ID 1640 const exposedVariableDeclarations: VariableDeclaration[] = []; 1641 let firstExposedNonVariableDeclaration: NamedDeclaration | undefined; 1642 1643 const expression = !isReadonlyArray(targetRange.range) 1644 ? targetRange.range 1645 : targetRange.range.length === 1 && isExpressionStatement(targetRange.range[0]) 1646 ? targetRange.range[0].expression 1647 : undefined; 1648 1649 let expressionDiagnostic: Diagnostic | undefined; 1650 if (expression === undefined) { 1651 const statements = targetRange.range as readonly Statement[]; 1652 const start = first(statements).getStart(); 1653 const end = last(statements).end; 1654 expressionDiagnostic = createFileDiagnostic(sourceFile, start, end - start, Messages.expressionExpected); 1655 } 1656 else if (checker.getTypeAtLocation(expression).flags & (TypeFlags.Void | TypeFlags.Never)) { 1657 expressionDiagnostic = createDiagnosticForNode(expression, Messages.uselessConstantType); 1658 } 1659 1660 // initialize results 1661 for (const scope of scopes) { 1662 usagesPerScope.push({ usages: new Map<string, UsageEntry>(), typeParameterUsages: new Map<string, TypeParameter>(), substitutions: new Map<string, Expression>() }); 1663 substitutionsPerScope.push(new Map<string, Expression>()); 1664 1665 functionErrorsPerScope.push([]); 1666 1667 const constantErrors = []; 1668 if (expressionDiagnostic) { 1669 constantErrors.push(expressionDiagnostic); 1670 } 1671 if (isClassLike(scope) && isInJSFile(scope)) { 1672 constantErrors.push(createDiagnosticForNode(scope, Messages.cannotExtractToJSClass)); 1673 } 1674 if (isArrowFunction(scope) && !isBlock(scope.body)) { 1675 // TODO (https://github.com/Microsoft/TypeScript/issues/18924): allow this 1676 constantErrors.push(createDiagnosticForNode(scope, Messages.cannotExtractToExpressionArrowFunction)); 1677 } 1678 constantErrorsPerScope.push(constantErrors); 1679 } 1680 1681 const seenUsages = new Map<string, Usage>(); 1682 const target = isReadonlyArray(targetRange.range) ? factory.createBlock(targetRange.range) : targetRange.range; 1683 1684 const unmodifiedNode = isReadonlyArray(targetRange.range) ? first(targetRange.range) : targetRange.range; 1685 const inGenericContext = isInGenericContext(unmodifiedNode); 1686 1687 collectUsages(target); 1688 1689 // Unfortunately, this code takes advantage of the knowledge that the generated method 1690 // will use the contextual type of an expression as the return type of the extracted 1691 // method (and will therefore "use" all the types involved). 1692 if (inGenericContext && !isReadonlyArray(targetRange.range) && !isJsxAttribute(targetRange.range)) { 1693 const contextualType = checker.getContextualType(targetRange.range)!; // TODO: GH#18217 1694 recordTypeParameterUsages(contextualType); 1695 } 1696 1697 if (allTypeParameterUsages.size > 0) { 1698 const seenTypeParameterUsages = new Map<string, TypeParameter>(); // Key is type ID 1699 1700 let i = 0; 1701 for (let curr: Node = unmodifiedNode; curr !== undefined && i < scopes.length; curr = curr.parent) { 1702 if (curr === scopes[i]) { 1703 // Copy current contents of seenTypeParameterUsages into scope. 1704 seenTypeParameterUsages.forEach((typeParameter, id) => { 1705 usagesPerScope[i].typeParameterUsages.set(id, typeParameter); 1706 }); 1707 1708 i++; 1709 } 1710 1711 // Note that we add the current node's type parameters *after* updating the corresponding scope. 1712 if (isDeclarationWithTypeParameters(curr)) { 1713 for (const typeParameterDecl of getEffectiveTypeParameterDeclarations(curr)) { 1714 const typeParameter = checker.getTypeAtLocation(typeParameterDecl) as TypeParameter; 1715 if (allTypeParameterUsages.has(typeParameter.id.toString())) { 1716 seenTypeParameterUsages.set(typeParameter.id.toString(), typeParameter); 1717 } 1718 } 1719 } 1720 } 1721 1722 // If we didn't get through all the scopes, then there were some that weren't in our 1723 // parent chain (impossible at time of writing). A conservative solution would be to 1724 // copy allTypeParameterUsages into all remaining scopes. 1725 Debug.assert(i === scopes.length, "Should have iterated all scopes"); 1726 } 1727 1728 // If there are any declarations in the extracted block that are used in the same enclosing 1729 // lexical scope, we can't move the extraction "up" as those declarations will become unreachable 1730 if (visibleDeclarationsInExtractedRange.length) { 1731 const containingLexicalScopeOfExtraction = isBlockScope(scopes[0], scopes[0].parent) 1732 ? scopes[0] 1733 : getEnclosingBlockScopeContainer(scopes[0]); 1734 forEachChild(containingLexicalScopeOfExtraction, checkForUsedDeclarations); 1735 } 1736 1737 for (let i = 0; i < scopes.length; i++) { 1738 const scopeUsages = usagesPerScope[i]; 1739 // Special case: in the innermost scope, all usages are available. 1740 // (The computed value reflects the value at the top-level of the scope, but the 1741 // local will actually be declared at the same level as the extracted expression). 1742 if (i > 0 && (scopeUsages.usages.size > 0 || scopeUsages.typeParameterUsages.size > 0)) { 1743 const errorNode = isReadonlyArray(targetRange.range) ? targetRange.range[0] : targetRange.range; 1744 constantErrorsPerScope[i].push(createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes)); 1745 } 1746 1747 if (targetRange.facts & RangeFacts.UsesThisInFunction && isClassLike(scopes[i])) { 1748 functionErrorsPerScope[i].push(createDiagnosticForNode(targetRange.thisNode!, Messages.cannotExtractFunctionsContainingThisToMethod)); 1749 } 1750 1751 let hasWrite = false; 1752 let readonlyClassPropertyWrite: Declaration | undefined; 1753 usagesPerScope[i].usages.forEach(value => { 1754 if (value.usage === Usage.Write) { 1755 hasWrite = true; 1756 if (value.symbol.flags & SymbolFlags.ClassMember && 1757 value.symbol.valueDeclaration && 1758 hasEffectiveModifier(value.symbol.valueDeclaration, ModifierFlags.Readonly)) { 1759 readonlyClassPropertyWrite = value.symbol.valueDeclaration; 1760 } 1761 } 1762 }); 1763 1764 // If an expression was extracted, then there shouldn't have been any variable declarations. 1765 Debug.assert(isReadonlyArray(targetRange.range) || exposedVariableDeclarations.length === 0, "No variable declarations expected if something was extracted"); 1766 1767 if (hasWrite && !isReadonlyArray(targetRange.range)) { 1768 const diag = createDiagnosticForNode(targetRange.range, Messages.cannotWriteInExpression); 1769 functionErrorsPerScope[i].push(diag); 1770 constantErrorsPerScope[i].push(diag); 1771 } 1772 else if (readonlyClassPropertyWrite && i > 0) { 1773 const diag = createDiagnosticForNode(readonlyClassPropertyWrite, Messages.cannotExtractReadonlyPropertyInitializerOutsideConstructor); 1774 functionErrorsPerScope[i].push(diag); 1775 constantErrorsPerScope[i].push(diag); 1776 } 1777 else if (firstExposedNonVariableDeclaration) { 1778 const diag = createDiagnosticForNode(firstExposedNonVariableDeclaration, Messages.cannotExtractExportedEntity); 1779 functionErrorsPerScope[i].push(diag); 1780 constantErrorsPerScope[i].push(diag); 1781 } 1782 } 1783 1784 return { target, usagesPerScope, functionErrorsPerScope, constantErrorsPerScope, exposedVariableDeclarations }; 1785 1786 function isInGenericContext(node: Node) { 1787 return !!findAncestor(node, n => isDeclarationWithTypeParameters(n) && getEffectiveTypeParameterDeclarations(n).length !== 0); 1788 } 1789 1790 function recordTypeParameterUsages(type: Type) { 1791 // PERF: This is potentially very expensive. `type` could be a library type with 1792 // a lot of properties, each of which the walker will visit. Unfortunately, the 1793 // solution isn't as trivial as filtering to user types because of (e.g.) Array. 1794 const symbolWalker = checker.getSymbolWalker(() => (cancellationToken.throwIfCancellationRequested(), true)); 1795 const { visitedTypes } = symbolWalker.walkType(type); 1796 1797 for (const visitedType of visitedTypes) { 1798 if (visitedType.isTypeParameter()) { 1799 allTypeParameterUsages.set(visitedType.id.toString(), visitedType); 1800 } 1801 } 1802 } 1803 1804 function collectUsages(node: Node, valueUsage = Usage.Read) { 1805 if (inGenericContext) { 1806 const type = checker.getTypeAtLocation(node); 1807 recordTypeParameterUsages(type); 1808 } 1809 1810 if (isDeclaration(node) && node.symbol) { 1811 visibleDeclarationsInExtractedRange.push(node); 1812 } 1813 1814 if (isAssignmentExpression(node)) { 1815 // use 'write' as default usage for values 1816 collectUsages(node.left, Usage.Write); 1817 collectUsages(node.right); 1818 } 1819 else if (isUnaryExpressionWithWrite(node)) { 1820 collectUsages(node.operand, Usage.Write); 1821 } 1822 else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) { 1823 // use 'write' as default usage for values 1824 forEachChild(node, collectUsages); 1825 } 1826 else if (isIdentifier(node)) { 1827 if (!node.parent) { 1828 return; 1829 } 1830 if (isQualifiedName(node.parent) && node !== node.parent.left) { 1831 return; 1832 } 1833 if (isPropertyAccessExpression(node.parent) && node !== node.parent.expression) { 1834 return; 1835 } 1836 recordUsage(node, valueUsage, /*isTypeNode*/ isPartOfTypeNode(node)); 1837 } 1838 else { 1839 forEachChild(node, collectUsages); 1840 } 1841 } 1842 1843 function recordUsage(n: Identifier, usage: Usage, isTypeNode: boolean) { 1844 const symbolId = recordUsagebySymbol(n, usage, isTypeNode); 1845 if (symbolId) { 1846 for (let i = 0; i < scopes.length; i++) { 1847 // push substitution from map<symbolId, subst> to map<nodeId, subst> to simplify rewriting 1848 const substitution = substitutionsPerScope[i].get(symbolId); 1849 if (substitution) { 1850 usagesPerScope[i].substitutions.set(getNodeId(n).toString(), substitution); 1851 } 1852 } 1853 } 1854 } 1855 1856 function recordUsagebySymbol(identifier: Identifier, usage: Usage, isTypeName: boolean) { 1857 const symbol = getSymbolReferencedByIdentifier(identifier); 1858 if (!symbol) { 1859 // cannot find symbol - do nothing 1860 return undefined; 1861 } 1862 const symbolId = getSymbolId(symbol).toString(); 1863 const lastUsage = seenUsages.get(symbolId); 1864 // there are two kinds of value usages 1865 // - reads - if range contains a read from the value located outside of the range then value should be passed as a parameter 1866 // - writes - if range contains a write to a value located outside the range the value should be passed as a parameter and 1867 // returned as a return value 1868 // 'write' case is a superset of 'read' so if we already have processed 'write' of some symbol there is not need to handle 'read' 1869 // since all information is already recorded 1870 if (lastUsage && lastUsage >= usage) { 1871 return symbolId; 1872 } 1873 1874 seenUsages.set(symbolId, usage); 1875 if (lastUsage) { 1876 // if we get here this means that we are trying to handle 'write' and 'read' was already processed 1877 // walk scopes and update existing records. 1878 for (const perScope of usagesPerScope) { 1879 const prevEntry = perScope.usages.get(identifier.text); 1880 if (prevEntry) { 1881 perScope.usages.set(identifier.text, { usage, symbol, node: identifier }); 1882 } 1883 } 1884 return symbolId; 1885 } 1886 // find first declaration in this file 1887 const decls = symbol.getDeclarations(); 1888 const declInFile = decls && find(decls, d => d.getSourceFile() === sourceFile); 1889 if (!declInFile) { 1890 return undefined; 1891 } 1892 if (rangeContainsStartEnd(enclosingTextRange, declInFile.getStart(), declInFile.end)) { 1893 // declaration is located in range to be extracted - do nothing 1894 return undefined; 1895 } 1896 if (targetRange.facts & RangeFacts.IsGenerator && usage === Usage.Write) { 1897 // this is write to a reference located outside of the target scope and range is extracted into generator 1898 // currently this is unsupported scenario 1899 const diag = createDiagnosticForNode(identifier, Messages.cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators); 1900 for (const errors of functionErrorsPerScope) { 1901 errors.push(diag); 1902 } 1903 for (const errors of constantErrorsPerScope) { 1904 errors.push(diag); 1905 } 1906 } 1907 for (let i = 0; i < scopes.length; i++) { 1908 const scope = scopes[i]; 1909 const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags, /*excludeGlobals*/ false); 1910 if (resolvedSymbol === symbol) { 1911 continue; 1912 } 1913 if (!substitutionsPerScope[i].has(symbolId)) { 1914 const substitution = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.exportSymbol || symbol, scope, isTypeName); 1915 if (substitution) { 1916 substitutionsPerScope[i].set(symbolId, substitution); 1917 } 1918 else if (isTypeName) { 1919 // If the symbol is a type parameter that won't be in scope, we'll pass it as a type argument 1920 // so there's no problem. 1921 if (!(symbol.flags & SymbolFlags.TypeParameter)) { 1922 const diag = createDiagnosticForNode(identifier, Messages.typeWillNotBeVisibleInTheNewScope); 1923 functionErrorsPerScope[i].push(diag); 1924 constantErrorsPerScope[i].push(diag); 1925 } 1926 } 1927 else { 1928 usagesPerScope[i].usages.set(identifier.text, { usage, symbol, node: identifier }); 1929 } 1930 } 1931 } 1932 return symbolId; 1933 } 1934 1935 function checkForUsedDeclarations(node: Node) { 1936 // If this node is entirely within the original extraction range, we don't need to do anything. 1937 if (node === targetRange.range || (isReadonlyArray(targetRange.range) && targetRange.range.indexOf(node as Statement) >= 0)) { 1938 return; 1939 } 1940 1941 // Otherwise check and recurse. 1942 const sym = isIdentifier(node) 1943 ? getSymbolReferencedByIdentifier(node) 1944 : checker.getSymbolAtLocation(node); 1945 if (sym) { 1946 const decl = find(visibleDeclarationsInExtractedRange, d => d.symbol === sym); 1947 if (decl) { 1948 if (isVariableDeclaration(decl)) { 1949 const idString = decl.symbol.id!.toString(); 1950 if (!exposedVariableSymbolSet.has(idString)) { 1951 exposedVariableDeclarations.push(decl); 1952 exposedVariableSymbolSet.set(idString, true); 1953 } 1954 } 1955 else { 1956 // CONSIDER: this includes binding elements, which we could 1957 // expose in the same way as variables. 1958 firstExposedNonVariableDeclaration = firstExposedNonVariableDeclaration || decl; 1959 } 1960 } 1961 } 1962 1963 forEachChild(node, checkForUsedDeclarations); 1964 } 1965 1966 /** 1967 * Return the symbol referenced by an identifier (even if it declares a different symbol). 1968 */ 1969 function getSymbolReferencedByIdentifier(identifier: Identifier) { 1970 // If the identifier is both a property name and its value, we're only interested in its value 1971 // (since the name is a declaration and will be included in the extracted range). 1972 return identifier.parent && isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier 1973 ? checker.getShorthandAssignmentValueSymbol(identifier.parent) 1974 : checker.getSymbolAtLocation(identifier); 1975 } 1976 1977 function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: Symbol | undefined, scopeDecl: Node, isTypeNode: boolean): PropertyAccessExpression | EntityName | undefined { 1978 if (!symbol) { 1979 return undefined; 1980 } 1981 const decls = symbol.getDeclarations(); 1982 if (decls && decls.some(d => d.parent === scopeDecl)) { 1983 return factory.createIdentifier(symbol.name); 1984 } 1985 const prefix = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.parent, scopeDecl, isTypeNode); 1986 if (prefix === undefined) { 1987 return undefined; 1988 } 1989 return isTypeNode 1990 ? factory.createQualifiedName(prefix as EntityName, factory.createIdentifier(symbol.name)) 1991 : factory.createPropertyAccessExpression(prefix as Expression, symbol.name); 1992 } 1993 } 1994 1995 function getExtractableParent(node: Node | undefined): Node | undefined { 1996 return findAncestor(node, node => node.parent && isExtractableExpression(node) && !isBinaryExpression(node.parent)); 1997 } 1998 1999 /** 2000 * Computes whether or not a node represents an expression in a position where it could 2001 * be extracted. 2002 * The isExpression() in utilities.ts returns some false positives we need to handle, 2003 * such as `import x from 'y'` -- the 'y' is a StringLiteral but is *not* an expression 2004 * in the sense of something that you could extract on 2005 */ 2006 function isExtractableExpression(node: Node): boolean { 2007 const { parent } = node; 2008 switch (parent.kind) { 2009 case SyntaxKind.EnumMember: 2010 return false; 2011 } 2012 2013 switch (node.kind) { 2014 case SyntaxKind.StringLiteral: 2015 return parent.kind !== SyntaxKind.ImportDeclaration && 2016 parent.kind !== SyntaxKind.ImportSpecifier; 2017 2018 case SyntaxKind.SpreadElement: 2019 case SyntaxKind.ObjectBindingPattern: 2020 case SyntaxKind.BindingElement: 2021 return false; 2022 2023 case SyntaxKind.Identifier: 2024 return parent.kind !== SyntaxKind.BindingElement && 2025 parent.kind !== SyntaxKind.ImportSpecifier && 2026 parent.kind !== SyntaxKind.ExportSpecifier; 2027 } 2028 return true; 2029 } 2030 2031 function isBlockLike(node: Node): node is BlockLike { 2032 switch (node.kind) { 2033 case SyntaxKind.Block: 2034 case SyntaxKind.SourceFile: 2035 case SyntaxKind.ModuleBlock: 2036 case SyntaxKind.CaseClause: 2037 return true; 2038 default: 2039 return false; 2040 } 2041 } 2042 2043 function isInJSXContent(node: Node) { 2044 return isStringLiteralJsxAttribute(node) || 2045 (isJsxElement(node) || isJsxSelfClosingElement(node) || isJsxFragment(node)) && (isJsxElement(node.parent) || isJsxFragment(node.parent)); 2046 } 2047 2048 function isStringLiteralJsxAttribute(node: Node): node is StringLiteral { 2049 return isStringLiteral(node) && node.parent && isJsxAttribute(node.parent); 2050 } 2051} 2052