Merge branch 'master' into watchImprovements

This commit is contained in:
Sheetal Nandi
2017-09-29 10:43:05 -07:00
197 changed files with 18472 additions and 13260 deletions

View File

@@ -156,8 +156,9 @@ namespace ts.codefix {
const propertyChangeTracker = textChanges.ChangeTracker.fromContext(context);
propertyChangeTracker.insertNodeAfter(classDeclarationSourceFile, classOpenBrace, property, { suffix: context.newLineCharacter });
(actions || (actions = [])).push({
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Declare_property_0), [tokenName]),
const diag = makeStatic ? Diagnostics.Declare_static_property_0 : Diagnostics.Declare_property_0;
actions = append(actions, {
description: formatStringFromArgs(getLocaleSpecificMessage(diag), [tokenName]),
changes: propertyChangeTracker.getChanges()
});
@@ -197,11 +198,9 @@ namespace ts.codefix {
const methodDeclarationChangeTracker = textChanges.ChangeTracker.fromContext(context);
methodDeclarationChangeTracker.insertNodeAfter(classDeclarationSourceFile, classOpenBrace, methodDeclaration, { suffix: context.newLineCharacter });
const diag = makeStatic ? Diagnostics.Declare_static_method_0 : Diagnostics.Declare_method_0;
return {
description: formatStringFromArgs(getLocaleSpecificMessage(makeStatic ?
Diagnostics.Declare_method_0 :
Diagnostics.Declare_static_method_0),
[tokenName]),
description: formatStringFromArgs(getLocaleSpecificMessage(diag), [tokenName]),
changes: methodDeclarationChangeTracker.getChanges()
};
}

View File

@@ -581,11 +581,10 @@ namespace ts.Completions {
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression {
switch (tag.kind) {
case SyntaxKind.JSDocAugmentsTag:
case SyntaxKind.JSDocParameterTag:
case SyntaxKind.JSDocPropertyTag:
case SyntaxKind.JSDocReturnTag:

View File

@@ -663,9 +663,10 @@ namespace ts.formatting {
undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line;
}
// if child is a list item - try to get its indentation
// if child is a list item - try to get its indentation, only if parent is within the original range.
let childIndentationAmount = Constants.Unknown;
if (isListItem) {
if (isListItem && rangeContainsRange(originalRange, parent)) {
childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation);
if (childIndentationAmount !== Constants.Unknown) {
inheritedIndentation = childIndentationAmount;

View File

@@ -2,18 +2,21 @@
/// <reference path="../../compiler/checker.ts" />
/* @internal */
namespace ts.refactor.extractMethod {
const extractMethod: Refactor = {
name: "Extract Method",
description: Diagnostics.Extract_function.message,
namespace ts.refactor.extractSymbol {
const extractSymbol: Refactor = {
name: "Extract Symbol",
description: Diagnostics.Extract_symbol.message,
getAvailableActions,
getEditsForAction,
};
registerRefactor(extractMethod);
registerRefactor(extractSymbol);
/** Compute the associated code actions */
function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
/**
* Compute the associated code actions
* Exported for tests.
*/
export function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length: getRefactorContextLength(context) });
const targetRange: TargetRange = rangeToExtract.targetRange;
@@ -27,63 +30,103 @@ namespace ts.refactor.extractMethod {
return undefined;
}
const actions: RefactorActionInfo[] = [];
const usedNames: Map<boolean> = createMap();
const functionActions: RefactorActionInfo[] = [];
const usedFunctionNames: Map<boolean> = createMap();
const constantActions: RefactorActionInfo[] = [];
const usedConstantNames: Map<boolean> = createMap();
let i = 0;
for (const { scopeDescription, errors } of extractions) {
for (const extraction of extractions) {
// Skip these since we don't have a way to report errors yet
if (errors.length) {
continue;
if (extraction.functionErrors.length === 0) {
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
const description = formatStringFromArgs(Diagnostics.Extract_to_0.message, [extraction.functionDescription]);
if (!usedFunctionNames.has(description)) {
usedFunctionNames.set(description, true);
functionActions.push({
description,
name: `function_scope_${i}`
});
}
}
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
const description = formatStringFromArgs(Diagnostics.Extract_to_0.message, [scopeDescription]);
if (!usedNames.has(description)) {
usedNames.set(description, true);
actions.push({
description,
name: `scope_${i}`
});
// Skip these since we don't have a way to report errors yet
if (extraction.constantErrors.length === 0) {
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
const description = formatStringFromArgs(Diagnostics.Extract_to_0.message, [extraction.constantDescription]);
if (!usedConstantNames.has(description)) {
usedConstantNames.set(description, true);
constantActions.push({
description,
name: `constant_scope_${i}`
});
}
}
// *do* increment i anyway because we'll look for the i-th scope
// later when actually doing the refactoring if the user requests it
i++;
}
if (actions.length === 0) {
return undefined;
const infos: ApplicableRefactorInfo[] = [];
if (functionActions.length) {
infos.push({
name: extractSymbol.name,
description: Diagnostics.Extract_function.message,
actions: functionActions
});
}
return [{
name: extractMethod.name,
description: extractMethod.description,
inlineable: true,
actions
}];
if (constantActions.length) {
infos.push({
name: extractSymbol.name,
description: Diagnostics.Extract_constant.message,
actions: constantActions
});
}
return infos.length ? infos : undefined;
}
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
/* Exported for tests */
export function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length: getRefactorContextLength(context) });
const targetRange: TargetRange = rangeToExtract.targetRange;
const parsedIndexMatch = /^scope_(\d+)$/.exec(actionName);
Debug.assert(!!parsedIndexMatch, "Scope name should have matched the regexp");
const index = +parsedIndexMatch[1];
Debug.assert(isFinite(index), "Expected to parse a finite number from the scope index");
const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName);
if (parsedFunctionIndexMatch) {
const index = +parsedFunctionIndexMatch[1];
Debug.assert(isFinite(index), "Expected to parse a finite number from the function scope index");
return getFunctionExtractionAtIndex(targetRange, context, index);
}
return getExtractionAtIndex(targetRange, context, index);
const parsedConstantIndexMatch = /^constant_scope_(\d+)$/.exec(actionName);
if (parsedConstantIndexMatch) {
const index = +parsedConstantIndexMatch[1];
Debug.assert(isFinite(index), "Expected to parse a finite number from the constant scope index");
return getConstantExtractionAtIndex(targetRange, context, index);
}
Debug.fail("Unrecognized action name");
}
// Move these into diagnostic messages if they become user-facing
namespace Messages {
export namespace Messages {
function createMessage(message: string): DiagnosticMessage {
return { message, code: 0, category: DiagnosticCategory.Message, key: message };
}
export const CannotExtractFunction: DiagnosticMessage = createMessage("Cannot extract function.");
export const CannotExtractRange: DiagnosticMessage = createMessage("Cannot extract range.");
export const CannotExtractImport: DiagnosticMessage = createMessage("Cannot extract import statement.");
export const CannotExtractSuper: DiagnosticMessage = createMessage("Cannot extract super call.");
export const CannotExtractEmpty: DiagnosticMessage = createMessage("Cannot extract empty range.");
export const ExpressionExpected: DiagnosticMessage = createMessage("expression expected.");
export const StatementOrExpressionExpected: DiagnosticMessage = createMessage("Statement or expression expected.");
export const CannotExtractRangeContainingConditionalBreakOrContinueStatements: DiagnosticMessage = createMessage("Cannot extract range containing conditional break or continue statements.");
export const CannotExtractRangeContainingConditionalReturnStatement: DiagnosticMessage = createMessage("Cannot extract range containing conditional return statement.");
@@ -91,11 +134,14 @@ namespace ts.refactor.extractMethod {
export const CannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators: DiagnosticMessage = createMessage("Cannot extract range containing writes to references located outside of the target range in generators.");
export const TypeWillNotBeVisibleInTheNewScope = createMessage("Type will not visible in the new scope.");
export const FunctionWillNotBeVisibleInTheNewScope = createMessage("Function will not visible in the new scope.");
export const InsufficientSelection = createMessage("Select more than a single identifier.");
export const CannotExtractIdentifier = createMessage("Select more than a single identifier.");
export const CannotExtractExportedEntity = createMessage("Cannot extract exported declaration");
export const CannotCombineWritesAndReturns = createMessage("Cannot combine writes and returns");
export const CannotExtractReadonlyPropertyInitializerOutsideConstructor = createMessage("Cannot move initialization of read-only class property outside of the constructor");
export const CannotExtractAmbientBlock = createMessage("Cannot extract code from ambient contexts");
export const CannotAccessVariablesFromNestedScopes = createMessage("Cannot access variables from nested scopes");
export const CannotExtractToOtherFunctionLike = createMessage("Cannot extract method to a function-like scope that is not a function");
export const CannotExtractToJSClass = createMessage("Cannot extract constant to a class scope in JS");
}
enum RangeFacts {
@@ -150,7 +196,7 @@ namespace ts.refactor.extractMethod {
const { length } = span;
if (length === 0) {
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.StatementOrExpressionExpected)] };
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.CannotExtractEmpty)] };
}
// Walk up starting from the the start position until we find a non-SourceFile node that subsumes the selected span.
@@ -167,7 +213,7 @@ namespace ts.refactor.extractMethod {
if (!start || !end) {
// cannot find either start or end node
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.CannotExtractFunction)] };
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.CannotExtractRange)] };
}
if (start.parent !== end.parent) {
@@ -193,13 +239,13 @@ namespace ts.refactor.extractMethod {
}
else {
// start and end nodes belong to different subtrees
return createErrorResult(sourceFile, span.start, length, Messages.CannotExtractFunction);
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.CannotExtractRange)] };
}
}
if (start !== end) {
// start and end should be statements and parent should be either block or a source file
if (!isBlockLike(start.parent)) {
return createErrorResult(sourceFile, span.start, length, Messages.CannotExtractFunction);
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.CannotExtractRange)] };
}
const statements: Statement[] = [];
for (const statement of (<BlockLike>start.parent).statements) {
@@ -216,22 +262,17 @@ namespace ts.refactor.extractMethod {
}
return { targetRange: { range: statements, facts: rangeFacts, declarations } };
}
else {
// We have a single node (start)
const errors = checkRootNode(start) || checkNode(start);
if (errors) {
return { errors };
}
return { targetRange: { range: getStatementOrExpressionRange(start), facts: rangeFacts, declarations } };
}
function createErrorResult(sourceFile: SourceFile, start: number, length: number, message: DiagnosticMessage): RangeToExtract {
return { errors: [createFileDiagnostic(sourceFile, start, length, message)] };
// We have a single node (start)
const errors = checkRootNode(start) || checkNode(start);
if (errors) {
return { errors };
}
return { targetRange: { range: getStatementOrExpressionRange(start), facts: rangeFacts, declarations } };
function checkRootNode(node: Node): Diagnostic[] | undefined {
if (isIdentifier(isExpressionStatement(node) ? node.expression : node)) {
return [createDiagnosticForNode(node, Messages.InsufficientSelection)];
return [createDiagnosticForNode(node, Messages.CannotExtractIdentifier)];
}
return undefined;
}
@@ -309,7 +350,7 @@ namespace ts.refactor.extractMethod {
// Some things can't be extracted in certain situations
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
(errors || (errors = [])).push(createDiagnosticForNode(node, Messages.CannotExtractFunction));
(errors || (errors = [])).push(createDiagnosticForNode(node, Messages.CannotExtractImport));
return true;
case SyntaxKind.SuperKeyword:
// For a super *constructor call*, we have to be extracting the entire class,
@@ -318,7 +359,7 @@ namespace ts.refactor.extractMethod {
// Super constructor call
const containingClass = getContainingClass(node);
if (containingClass.pos < span.start || containingClass.end >= (span.start + span.length)) {
(errors || (errors = [])).push(createDiagnosticForNode(node, Messages.CannotExtractFunction));
(errors || (errors = [])).push(createDiagnosticForNode(node, Messages.CannotExtractSuper));
return true;
}
}
@@ -328,7 +369,7 @@ namespace ts.refactor.extractMethod {
break;
}
if (!node || isFunctionLike(node) || isClassLike(node)) {
if (!node || isFunctionLikeDeclaration(node) || isClassLike(node)) {
switch (node.kind) {
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
@@ -439,9 +480,8 @@ namespace ts.refactor.extractMethod {
return undefined;
}
function isValidExtractionTarget(node: Node): node is Scope {
// Note that we don't use isFunctionLike because we don't want to put the extracted closure *inside* a method
return (node.kind === SyntaxKind.FunctionDeclaration) || isSourceFile(node) || isModuleBlock(node) || isClassLike(node);
function isScope(node: Node): node is Scope {
return isFunctionLikeDeclaration(node) || isSourceFile(node) || isModuleBlock(node) || isClassLike(node);
}
/**
@@ -468,14 +508,14 @@ namespace ts.refactor.extractMethod {
// * Function declaration
// * Class declaration or expression
// * Module/namespace or source file
if (current !== start && isValidExtractionTarget(current)) {
if (current !== start && isScope(current)) {
(scopes = scopes || []).push(current);
}
// A function parameter's initializer is actually in the outer scope, not the function declaration
if (current && current.parent && current.parent.kind === SyntaxKind.Parameter) {
// Skip all the way to the outer scope of the function that declared this parameter
current = findAncestor(current, parent => isFunctionLike(parent)).parent;
current = findAncestor(current, parent => isFunctionLikeDeclaration(parent)).parent;
}
else {
current = current.parent;
@@ -485,29 +525,44 @@ namespace ts.refactor.extractMethod {
return scopes;
}
// exported only for tests
export function getExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo {
const { scopes, readsAndWrites: { target, usagesPerScope, errorsPerScope } } = getPossibleExtractionsWorker(targetRange, context);
Debug.assert(!errorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?");
function getFunctionExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo {
const { scopes, readsAndWrites: { target, usagesPerScope, functionErrorsPerScope } } = getPossibleExtractionsWorker(targetRange, context);
Debug.assert(!functionErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?");
context.cancellationToken.throwIfCancellationRequested();
return extractFunctionInScope(target, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange, context);
}
function getConstantExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo {
const { scopes, readsAndWrites: { target, usagesPerScope, constantErrorsPerScope } } = getPossibleExtractionsWorker(targetRange, context);
Debug.assert(!constantErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?");
context.cancellationToken.throwIfCancellationRequested();
const expression = isExpression(target)
? target
: (target.statements[0] as ExpressionStatement).expression;
return extractConstantInScope(expression, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange.facts, context);
}
interface PossibleExtraction {
readonly scopeDescription: string;
readonly errors: ReadonlyArray<Diagnostic>;
readonly functionDescription: string;
readonly functionErrors: ReadonlyArray<Diagnostic>;
readonly constantDescription: string;
readonly constantErrors: ReadonlyArray<Diagnostic>;
}
/**
* Given a piece of text to extract ('targetRange'), computes a list of possible extractions.
* Each returned ExtractResultForScope corresponds to a possible target scope and is either a set of changes
* or an error explaining why we can't extract into that scope.
*/
// exported only for tests
export function getPossibleExtractions(targetRange: TargetRange, context: RefactorContext): ReadonlyArray<PossibleExtraction> | undefined {
const { scopes, readsAndWrites: { errorsPerScope } } = getPossibleExtractionsWorker(targetRange, context);
function getPossibleExtractions(targetRange: TargetRange, context: RefactorContext): ReadonlyArray<PossibleExtraction> | undefined {
const { scopes, readsAndWrites: { functionErrorsPerScope, constantErrorsPerScope } } = getPossibleExtractionsWorker(targetRange, context);
// Need the inner type annotation to avoid https://github.com/Microsoft/TypeScript/issues/7547
return scopes.map((scope, i): PossibleExtraction =>
({ scopeDescription: getDescriptionForScope(scope), errors: errorsPerScope[i] }));
const extractions = scopes.map((scope, i): PossibleExtraction => ({
functionDescription: getDescriptionForFunctionInScope(scope),
functionErrors: functionErrorsPerScope[i],
constantDescription: getDescriptionForConstantInScope(scope),
constantErrors: constantErrorsPerScope[i],
}));
return extractions;
}
function getPossibleExtractionsWorker(targetRange: TargetRange, context: RefactorContext): { readonly scopes: Scope[], readonly readsAndWrites: ReadsAndWrites } {
@@ -533,13 +588,20 @@ namespace ts.refactor.extractMethod {
return { scopes, readsAndWrites };
}
function getDescriptionForScope(scope: Scope): string {
function getDescriptionForFunctionInScope(scope: Scope): string {
return isFunctionLikeDeclaration(scope)
? `inner function in ${getDescriptionForFunctionLikeDeclaration(scope)}`
: isClassLike(scope)
? `method in ${getDescriptionForClassLikeDeclaration(scope)}`
: `function in ${getDescriptionForModuleLikeDeclaration(scope)}`;
}
function getDescriptionForConstantInScope(scope: Scope): string {
return isFunctionLikeDeclaration(scope)
? `constant in ${getDescriptionForFunctionLikeDeclaration(scope)}`
: isClassLike(scope)
? `readonly field in ${getDescriptionForClassLikeDeclaration(scope)}`
: `constant in ${getDescriptionForModuleLikeDeclaration(scope)}`;
}
function getDescriptionForFunctionLikeDeclaration(scope: FunctionLikeDeclaration): string {
switch (scope.kind) {
case SyntaxKind.Constructor:
@@ -573,12 +635,12 @@ namespace ts.refactor.extractMethod {
: scope.externalModuleIndicator ? "module scope" : "global scope";
}
function getUniqueName(fileText: string): string {
let functionNameText = "newFunction";
for (let i = 1; fileText.indexOf(functionNameText) !== -1; i++) {
functionNameText = `newFunction_${i}`;
function getUniqueName(baseName: string, fileText: string): string {
let nameText = baseName;
for (let i = 1; fileText.indexOf(nameText) !== -1; i++) {
nameText = `${baseName}_${i}`;
}
return functionNameText;
return nameText;
}
/**
@@ -596,7 +658,7 @@ namespace ts.refactor.extractMethod {
// Make a unique name for the extracted function
const file = scope.getSourceFile();
const functionNameText = getUniqueName(file.text);
const functionNameText = getUniqueName(isClassLike(scope) ? "newMethod" : "newFunction", file.text);
const isJS = isInJavaScriptFile(scope);
const functionName = createIdentifier(functionNameText);
@@ -688,7 +750,7 @@ namespace ts.refactor.extractMethod {
const changeTracker = textChanges.ChangeTracker.fromContext(context);
const minInsertionPos = (isReadonlyArray(range.range) ? lastOrUndefined(range.range) : range.range).end;
const nodeToInsertBefore = getNodeToInsertBefore(minInsertionPos, scope);
const nodeToInsertBefore = getNodeToInsertFunctionBefore(minInsertionPos, scope);
if (nodeToInsertBefore) {
changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newFunction, { suffix: context.newLineCharacter + context.newLineCharacter });
}
@@ -774,27 +836,126 @@ namespace ts.refactor.extractMethod {
const renameRange = isReadonlyArray(range.range) ? range.range[0] : range.range;
const renameFilename = renameRange.getSourceFile().fileName;
const renameLocation = getRenameLocation(edits, renameFilename, functionNameText);
const renameLocation = getRenameLocation(edits, renameFilename, functionNameText, /*isDeclaredBeforeUse*/ false);
return { renameFilename, renameLocation, edits };
}
function getRenameLocation(edits: ReadonlyArray<FileTextChanges>, renameFilename: string, functionNameText: string): number {
/**
* Result of 'extractRange' operation for a specific scope.
* Stores either a list of changes that should be applied to extract a range or a list of errors
*/
function extractConstantInScope(
node: Expression,
scope: Scope,
{ substitutions }: ScopeUsages,
rangeFacts: RangeFacts,
context: RefactorContext): RefactorEditInfo {
const checker = context.program.getTypeChecker();
// Make a unique name for the extracted variable
const file = scope.getSourceFile();
const localNameText = getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file.text);
const isJS = isInJavaScriptFile(scope);
const variableType = isJS
? undefined
: checker.typeToTypeNode(checker.getContextualType(node));
const initializer = transformConstantInitializer(node, substitutions);
const changeTracker = textChanges.ChangeTracker.fromContext(context);
if (isClassLike(scope)) {
Debug.assert(!isJS); // See CannotExtractToJSClass
const modifiers: Modifier[] = [];
modifiers.push(createToken(SyntaxKind.PrivateKeyword));
if (rangeFacts & RangeFacts.InStaticRegion) {
modifiers.push(createToken(SyntaxKind.StaticKeyword));
}
modifiers.push(createToken(SyntaxKind.ReadonlyKeyword));
const newVariable = createProperty(
/*decorators*/ undefined,
modifiers,
localNameText,
/*questionToken*/ undefined,
variableType,
initializer);
const localReference = createPropertyAccess(
rangeFacts & RangeFacts.InStaticRegion
? createIdentifier(scope.name.getText())
: createThis(),
createIdentifier(localNameText));
// Declare
const minInsertionPos = node.end;
const nodeToInsertBefore = getNodeToInsertConstantBefore(minInsertionPos, scope);
changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, { suffix: context.newLineCharacter + context.newLineCharacter });
// Consume
changeTracker.replaceNodeWithNodes(context.file, node, [localReference], { nodeSeparator: context.newLineCharacter });
}
else {
const newVariable = createVariableStatement(
/*modifiers*/ undefined,
createVariableDeclarationList(
[createVariableDeclaration(localNameText, variableType, initializer)],
NodeFlags.Const));
// If the parent is an expression statement, replace the statement with the declaration
if (node.parent.kind === SyntaxKind.ExpressionStatement) {
changeTracker.replaceNodeWithNodes(context.file, node.parent, [newVariable], { nodeSeparator: context.newLineCharacter });
}
else {
// Declare
const minInsertionPos = node.end;
const nodeToInsertBefore = getNodeToInsertConstantBefore(minInsertionPos, scope);
changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, { suffix: context.newLineCharacter + context.newLineCharacter });
// Consume
const localReference = createIdentifier(localNameText);
changeTracker.replaceNodeWithNodes(context.file, node, [localReference], { nodeSeparator: context.newLineCharacter });
}
}
const edits = changeTracker.getChanges();
const renameFilename = node.getSourceFile().fileName;
const renameLocation = getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true);
return { renameFilename, renameLocation, edits };
}
/**
* @return The index of the (only) reference to the extracted symbol. We want the cursor
* to be on the reference, rather than the declaration, because it's closer to where the
* user was before extracting it.
*/
function getRenameLocation(edits: ReadonlyArray<FileTextChanges>, renameFilename: string, functionNameText: string, isDeclaredBeforeUse: boolean): number {
let delta = 0;
let lastPos = -1;
for (const { fileName, textChanges } of edits) {
Debug.assert(fileName === renameFilename);
for (const change of textChanges) {
const { span, newText } = change;
// TODO(acasey): We are assuming that the call expression comes before the function declaration,
// because we want the new cursor to be on the call expression,
// which is closer to where the user was before extracting the function.
const index = newText.indexOf(functionNameText);
if (index !== -1) {
return span.start + delta + index;
lastPos = span.start + delta + index;
// If the reference comes first, return immediately.
if (!isDeclaredBeforeUse) {
return lastPos;
}
}
delta += newText.length - span.length;
}
}
throw new Error(); // Didn't find the text we inserted?
// If the declaration comes first, return the position of the last occurrence.
Debug.assert(isDeclaredBeforeUse);
Debug.assert(lastPos >= 0);
return lastPos;
}
function getFirstDeclaration(type: Type): Declaration | undefined {
@@ -899,7 +1060,7 @@ namespace ts.refactor.extractMethod {
}
else {
const oldIgnoreReturns = ignoreReturns;
ignoreReturns = ignoreReturns || isFunctionLike(node) || isClassLike(node);
ignoreReturns = ignoreReturns || isFunctionLikeDeclaration(node) || isClassLike(node);
const substitution = substitutions.get(getNodeId(node).toString());
const result = substitution || visitEachChild(node, visitor, nullTransformationContext);
ignoreReturns = oldIgnoreReturns;
@@ -908,8 +1069,19 @@ namespace ts.refactor.extractMethod {
}
}
function transformConstantInitializer(initializer: Expression, substitutions: ReadonlyMap<Node>): Expression {
return substitutions.size
? visitor(initializer) as Expression
: initializer;
function visitor(node: Node): VisitResult<Node> {
const substitution = substitutions.get(getNodeId(node).toString());
return substitution || visitEachChild(node, visitor, nullTransformationContext);
}
}
function getStatementsOrClassElements(scope: Scope): ReadonlyArray<Statement> | ReadonlyArray<ClassElement> {
if (isFunctionLike(scope)) {
if (isFunctionLikeDeclaration(scope)) {
const body = scope.body;
if (isBlock(body)) {
return body.statements;
@@ -932,9 +1104,31 @@ namespace ts.refactor.extractMethod {
* If `scope` contains a function after `minPos`, then return the first such function.
* Otherwise, return `undefined`.
*/
function getNodeToInsertBefore(minPos: number, scope: Scope): Node | undefined {
function getNodeToInsertFunctionBefore(minPos: number, scope: Scope): Node | undefined {
return find<Statement | ClassElement>(getStatementsOrClassElements(scope), child =>
child.pos >= minPos && isFunctionLike(child) && !isConstructorDeclaration(child));
child.pos >= minPos && isFunctionLikeDeclaration(child) && !isConstructorDeclaration(child));
}
// TODO (acasey): need to dig into nested statements
// TODO (acasey): don't insert before pinned comments, directives, or triple-slash references
function getNodeToInsertConstantBefore(maxPos: number, scope: Scope): Node {
const children = getStatementsOrClassElements(scope);
Debug.assert(children.length > 0); // There must be at least one child, since we extracted from one.
const isClassLikeScope = isClassLike(scope);
let prevChild: Statement | ClassElement | undefined = undefined;
for (const child of children) {
if (child.pos >= maxPos) {
break;
}
prevChild = child;
if (isClassLikeScope && !isPropertyDeclaration(child)) {
break;
}
}
Debug.assert(prevChild !== undefined);
return prevChild;
}
function getPropertyAssignmentsForWrites(writes: ReadonlyArray<UsageEntry>): ShorthandPropertyAssignment[] {
@@ -982,7 +1176,8 @@ namespace ts.refactor.extractMethod {
interface ReadsAndWrites {
readonly target: Expression | Block;
readonly usagesPerScope: ReadonlyArray<ScopeUsages>;
readonly errorsPerScope: ReadonlyArray<ReadonlyArray<Diagnostic>>;
readonly functionErrorsPerScope: ReadonlyArray<ReadonlyArray<Diagnostic>>;
readonly constantErrorsPerScope: ReadonlyArray<ReadonlyArray<Diagnostic>>;
}
function collectReadsAndWrites(
targetRange: TargetRange,
@@ -995,14 +1190,33 @@ namespace ts.refactor.extractMethod {
const allTypeParameterUsages = createMap<TypeParameter>(); // Key is type ID
const usagesPerScope: ScopeUsages[] = [];
const substitutionsPerScope: Map<Node>[] = [];
const errorsPerScope: Diagnostic[][] = [];
const functionErrorsPerScope: Diagnostic[][] = [];
const constantErrorsPerScope: Diagnostic[][] = [];
const visibleDeclarationsInExtractedRange: Symbol[] = [];
const expressionDiagnostic =
isReadonlyArray(targetRange.range) && !(targetRange.range.length === 1 && isExpressionStatement(targetRange.range[0]))
? ((start, end) => createFileDiagnostic(sourceFile, start, end - start, Messages.ExpressionExpected))(firstOrUndefined(targetRange.range).getStart(), lastOrUndefined(targetRange.range).end)
: undefined;
// initialize results
for (const _ of scopes) {
for (const scope of scopes) {
usagesPerScope.push({ usages: createMap<UsageEntry>(), typeParameterUsages: createMap<TypeParameter>(), substitutions: createMap<Expression>() });
substitutionsPerScope.push(createMap<Expression>());
errorsPerScope.push([]);
functionErrorsPerScope.push(
isFunctionLikeDeclaration(scope) && scope.kind !== SyntaxKind.FunctionDeclaration
? [createDiagnosticForNode(scope, Messages.CannotExtractToOtherFunctionLike)]
: []);
const constantErrors = [];
if (expressionDiagnostic) {
constantErrors.push(expressionDiagnostic);
}
if (isClassLike(scope) && isInJavaScriptFile(scope)) {
constantErrors.push(createDiagnosticForNode(scope, Messages.CannotExtractToJSClass));
}
constantErrorsPerScope.push(constantErrors);
}
const seenUsages = createMap<Usage>();
@@ -1054,6 +1268,13 @@ namespace ts.refactor.extractMethod {
}
for (let i = 0; i < scopes.length; i++) {
if (!isReadonlyArray(targetRange.range)) {
const scopeUsages = usagesPerScope[i];
if (scopeUsages.usages.size > 0 || scopeUsages.typeParameterUsages.size > 0) {
constantErrorsPerScope[i].push(createDiagnosticForNode(targetRange.range, Messages.CannotAccessVariablesFromNestedScopes));
}
}
let hasWrite = false;
let readonlyClassPropertyWrite: Declaration | undefined = undefined;
usagesPerScope[i].usages.forEach(value => {
@@ -1068,10 +1289,14 @@ namespace ts.refactor.extractMethod {
});
if (hasWrite && !isReadonlyArray(targetRange.range) && isExpression(targetRange.range)) {
errorsPerScope[i].push(createDiagnosticForNode(targetRange.range, Messages.CannotCombineWritesAndReturns));
const diag = createDiagnosticForNode(targetRange.range, Messages.CannotCombineWritesAndReturns);
functionErrorsPerScope[i].push(diag);
constantErrorsPerScope[i].push(diag);
}
else if (readonlyClassPropertyWrite && i > 0) {
errorsPerScope[i].push(createDiagnosticForNode(readonlyClassPropertyWrite, Messages.CannotExtractReadonlyPropertyInitializerOutsideConstructor));
const diag = createDiagnosticForNode(readonlyClassPropertyWrite, Messages.CannotExtractReadonlyPropertyInitializerOutsideConstructor);
functionErrorsPerScope[i].push(diag);
constantErrorsPerScope[i].push(diag);
}
}
@@ -1081,7 +1306,7 @@ namespace ts.refactor.extractMethod {
forEachChild(containingLexicalScopeOfExtraction, checkForUsedDeclarations);
}
return { target, usagesPerScope, errorsPerScope };
return { target, usagesPerScope, functionErrorsPerScope, constantErrorsPerScope };
function hasTypeParameters(node: Node) {
return isDeclarationWithTypeParameters(node) &&
@@ -1157,9 +1382,9 @@ namespace ts.refactor.extractMethod {
if (symbolId) {
for (let i = 0; i < scopes.length; i++) {
// push substitution from map<symbolId, subst> to map<nodeId, subst> to simplify rewriting
const substitition = substitutionsPerScope[i].get(symbolId);
if (substitition) {
usagesPerScope[i].substitutions.set(getNodeId(n).toString(), substitition);
const substitution = substitutionsPerScope[i].get(symbolId);
if (substitution) {
usagesPerScope[i].substitutions.set(getNodeId(n).toString(), substitution);
}
}
}
@@ -1211,8 +1436,12 @@ namespace ts.refactor.extractMethod {
if (targetRange.facts & RangeFacts.IsGenerator && usage === Usage.Write) {
// this is write to a reference located outside of the target scope and range is extracted into generator
// currently this is unsupported scenario
for (const errors of errorsPerScope) {
errors.push(createDiagnosticForNode(identifier, Messages.CannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators));
const diag = createDiagnosticForNode(identifier, Messages.CannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators);
for (const errors of functionErrorsPerScope) {
errors.push(diag);
}
for (const errors of constantErrorsPerScope) {
errors.push(diag);
}
}
for (let i = 0; i < scopes.length; i++) {
@@ -1230,7 +1459,9 @@ namespace ts.refactor.extractMethod {
// If the symbol is a type parameter that won't be in scope, we'll pass it as a type argument
// so there's no problem.
if (!(symbol.flags & SymbolFlags.TypeParameter)) {
errorsPerScope[i].push(createDiagnosticForNode(identifier, Messages.TypeWillNotBeVisibleInTheNewScope));
const diag = createDiagnosticForNode(identifier, Messages.TypeWillNotBeVisibleInTheNewScope);
functionErrorsPerScope[i].push(diag);
constantErrorsPerScope[i].push(diag);
}
}
else {
@@ -1250,8 +1481,12 @@ namespace ts.refactor.extractMethod {
// Otherwise check and recurse.
const sym = checker.getSymbolAtLocation(node);
if (sym && visibleDeclarationsInExtractedRange.some(d => d === sym)) {
for (const scope of errorsPerScope) {
scope.push(createDiagnosticForNode(node, Messages.CannotExtractExportedEntity));
const diag = createDiagnosticForNode(node, Messages.CannotExtractExportedEntity);
for (const errors of functionErrorsPerScope) {
errors.push(diag);
}
for (const errors of constantErrorsPerScope) {
errors.push(diag);
}
return true;
}

View File

@@ -1,2 +1,2 @@
/// <reference path="convertFunctionToEs6Class.ts" />
/// <reference path="extractMethod.ts" />
/// <reference path="extractSymbol.ts" />

View File

@@ -327,7 +327,7 @@ namespace ts {
}
get name(): string {
return unescapeLeadingUnderscores(this.escapedName);
return symbolName(this);
}
getEscapedName(): __String {
@@ -383,7 +383,7 @@ namespace ts {
}
get text(): string {
return unescapeLeadingUnderscores(this.escapedText);
return idText(this);
}
}
IdentifierObject.prototype.kind = SyntaxKind.Identifier;