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