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