mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-04 21:53:42 -06:00
Add inline variable refactor (#54281)
This commit is contained in:
parent
89cbea8e16
commit
31936ea36e
@ -7632,6 +7632,18 @@
|
||||
"category": "Message",
|
||||
"code": 95183
|
||||
},
|
||||
"Inline variable": {
|
||||
"category": "Message",
|
||||
"code": 95184
|
||||
},
|
||||
"Could not find variable to inline.": {
|
||||
"category": "Message",
|
||||
"code": 95185
|
||||
},
|
||||
"Variables with multiple declarations cannot be inlined.": {
|
||||
"category": "Message",
|
||||
"code": 95186
|
||||
},
|
||||
|
||||
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
|
||||
"category": "Error",
|
||||
|
||||
@ -5,6 +5,7 @@ export * from "../refactors/convertExport";
|
||||
export * from "../refactors/convertImport";
|
||||
export * from "../refactors/extractType";
|
||||
export * from "../refactors/helpers";
|
||||
export * from "../refactors/inlineVariable";
|
||||
export * from "../refactors/moveToNewFile";
|
||||
export * from "../refactors/moveToFile";
|
||||
import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction";
|
||||
|
||||
@ -849,8 +849,12 @@ export function getTextSpanOfEntry(entry: Entry) {
|
||||
getTextSpan(entry.node, entry.node.getSourceFile());
|
||||
}
|
||||
|
||||
/** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */
|
||||
function isWriteAccessForReference(node: Node): boolean {
|
||||
/**
|
||||
* A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function isWriteAccessForReference(node: Node): boolean {
|
||||
const decl = getDeclarationFromName(node);
|
||||
return !!decl && declarationIsWriteAccess(decl) || node.kind === SyntaxKind.DefaultKeyword || isWriteAccess(node);
|
||||
}
|
||||
|
||||
236
src/services/refactors/inlineVariable.ts
Normal file
236
src/services/refactors/inlineVariable.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import {
|
||||
cast,
|
||||
Debug,
|
||||
Diagnostics,
|
||||
emptyArray,
|
||||
Expression,
|
||||
factory,
|
||||
FindAllReferences,
|
||||
getExpressionPrecedence,
|
||||
getLocaleSpecificMessage,
|
||||
getSynthesizedDeepClone,
|
||||
getTouchingPropertyName,
|
||||
Identifier,
|
||||
InitializedVariableDeclaration,
|
||||
isCallLikeExpression,
|
||||
isExportAssignment,
|
||||
isExportModifier,
|
||||
isExportSpecifier,
|
||||
isExpression,
|
||||
isFunctionLike,
|
||||
isIdentifier,
|
||||
isInitializedVariable,
|
||||
isTypeQueryNode,
|
||||
isVariableDeclarationInVariableStatement,
|
||||
isVariableStatement,
|
||||
needsParentheses,
|
||||
Node,
|
||||
Program,
|
||||
refactor,
|
||||
some,
|
||||
SourceFile,
|
||||
SymbolFlags,
|
||||
textChanges,
|
||||
textRangeContainsPositionInclusive,
|
||||
TypeChecker,
|
||||
VariableDeclaration,
|
||||
} from "../_namespaces/ts";
|
||||
import { RefactorErrorInfo, registerRefactor } from "../_namespaces/ts.refactor";
|
||||
|
||||
const refactorName = "Inline variable";
|
||||
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Inline_variable);
|
||||
|
||||
const inlineVariableAction = {
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
kind: "refactor.inline.variable"
|
||||
};
|
||||
|
||||
interface InliningInfo {
|
||||
references: Node[];
|
||||
declaration: VariableDeclaration;
|
||||
replacement: Expression;
|
||||
}
|
||||
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [inlineVariableAction.kind],
|
||||
|
||||
getAvailableActions(context) {
|
||||
const {
|
||||
file,
|
||||
program,
|
||||
preferences,
|
||||
startPosition,
|
||||
triggerReason
|
||||
} = context;
|
||||
|
||||
// tryWithReferenceToken is true below when triggerReason === "invoked", since we want to
|
||||
// always provide the refactor in the declaration site but only show it in references when
|
||||
// the refactor is explicitly invoked.
|
||||
const info = getInliningInfo(file, startPosition, triggerReason === "invoked", program);
|
||||
if (!info) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
if (!refactor.isRefactorErrorInfo(info)) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [inlineVariableAction]
|
||||
}];
|
||||
}
|
||||
|
||||
if (preferences.provideRefactorNotApplicableReason) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [{
|
||||
...inlineVariableAction,
|
||||
notApplicableReason: info.error
|
||||
}]
|
||||
}];
|
||||
}
|
||||
|
||||
return emptyArray;
|
||||
},
|
||||
|
||||
getEditsForAction(context, actionName) {
|
||||
Debug.assert(actionName === refactorName, "Unexpected refactor invoked");
|
||||
|
||||
const { file, program, startPosition } = context;
|
||||
|
||||
// tryWithReferenceToken is true below since here we're already performing the refactor.
|
||||
// The trigger kind was only relevant when checking the availability of the refactor.
|
||||
const info = getInliningInfo(file, startPosition, /*tryWithReferenceToken*/ true, program);
|
||||
if (!info || refactor.isRefactorErrorInfo(info)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { references, declaration, replacement } = info;
|
||||
const edits = textChanges.ChangeTracker.with(context, tracker => {
|
||||
for (const node of references) {
|
||||
tracker.replaceNode(file, node, getReplacementExpression(node, replacement));
|
||||
}
|
||||
tracker.delete(file, declaration);
|
||||
});
|
||||
|
||||
return { edits };
|
||||
}
|
||||
});
|
||||
|
||||
function getInliningInfo(file: SourceFile, startPosition: number, tryWithReferenceToken: boolean, program: Program): InliningInfo | RefactorErrorInfo | undefined {
|
||||
const checker = program.getTypeChecker();
|
||||
const token = getTouchingPropertyName(file, startPosition);
|
||||
const parent = token.parent;
|
||||
|
||||
if (!isIdentifier(token)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If triggered in a variable declaration, make sure it's not in a catch clause or for-loop
|
||||
// and that it has a value.
|
||||
if (isInitializedVariable(parent) && isVariableDeclarationInVariableStatement(parent)) {
|
||||
// Don't inline the variable if it has multiple declarations.
|
||||
if (checker.getMergedSymbol(parent.symbol).declarations?.length !== 1) {
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Variables_with_multiple_declarations_cannot_be_inlined) };
|
||||
}
|
||||
|
||||
// Do not inline if the variable is exported.
|
||||
if (isDeclarationExported(parent)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find all references to the variable in the current file.
|
||||
const references = getReferenceNodes(parent, checker, file);
|
||||
return references && { references, declaration: parent, replacement: parent.initializer };
|
||||
}
|
||||
|
||||
// Try finding the declaration and nodes to replace via the reference token.
|
||||
if (tryWithReferenceToken) {
|
||||
let definition = checker.resolveName(token.text, token, SymbolFlags.Value, /*excludeGlobals*/ false);
|
||||
definition = definition && checker.getMergedSymbol(definition);
|
||||
|
||||
// Don't inline the variable if it has multiple declarations.
|
||||
if (definition?.declarations?.length !== 1) {
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Variables_with_multiple_declarations_cannot_be_inlined) };
|
||||
}
|
||||
|
||||
// Make sure we're not inlining something like "let foo;" or "for (let i = 0; i < 5; i++) {}".
|
||||
const declaration = definition.declarations[0];
|
||||
if (!isInitializedVariable(declaration) || !isVariableDeclarationInVariableStatement(declaration) || !isIdentifier(declaration.name)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Do not inline if the variable is exported.
|
||||
if (isDeclarationExported(declaration)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const references = getReferenceNodes(declaration, checker, file);
|
||||
return references && { references, declaration, replacement: declaration.initializer };
|
||||
}
|
||||
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_variable_to_inline) };
|
||||
}
|
||||
|
||||
function isDeclarationExported(declaration: InitializedVariableDeclaration): boolean {
|
||||
// We use this function after isVariableDeclarationInVariableStatement, which ensures
|
||||
// that the cast below succeeds.
|
||||
const variableStatement = cast(declaration.parent.parent, isVariableStatement);
|
||||
return some(variableStatement.modifiers, isExportModifier);
|
||||
}
|
||||
|
||||
function getReferenceNodes(declaration: InitializedVariableDeclaration, checker: TypeChecker, file: SourceFile): Identifier[] | undefined {
|
||||
const references: Identifier[] = [];
|
||||
const cannotInline = FindAllReferences.Core.eachSymbolReferenceInFile(declaration.name as Identifier, checker, file, ref => {
|
||||
// Only inline if all references are reads. Else we might end up with an invalid scenario like:
|
||||
// const y = x++ + 1 -> const y = 2++ + 1
|
||||
if (FindAllReferences.isWriteAccessForReference(ref)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We cannot inline exported variables like "export { x as y }" or "export default foo".
|
||||
if (isExportSpecifier(ref.parent) || isExportAssignment(ref.parent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// typeof needs an identifier, so we can't inline a value in there.
|
||||
if (isTypeQueryNode(ref.parent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cannot inline recursive declarations (e.g. const foo = () => foo();)
|
||||
if (textRangeContainsPositionInclusive(declaration, ref.pos)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
references.push(ref);
|
||||
});
|
||||
|
||||
return references.length === 0 || cannotInline ? undefined : references;
|
||||
}
|
||||
|
||||
function getReplacementExpression(reference: Node, replacement: Expression): Expression {
|
||||
// Make sure each reference site gets its own copy of the replacement node.
|
||||
replacement = getSynthesizedDeepClone(replacement);
|
||||
const { parent } = reference;
|
||||
|
||||
// Logic from binaryOperandNeedsParentheses: "If the operand has lower precedence,
|
||||
// then it needs to be parenthesized to preserve the intent of the expression.
|
||||
// If the operand has higher precedence, then it does not need to be parenthesized."
|
||||
//
|
||||
// Note that binaryOperandNeedsParentheses has further logic when the precedences
|
||||
// are equal, but for the purposes of this refactor we keep things simple and
|
||||
// instead just check for special cases with needsParentheses.
|
||||
if (isExpression(parent) && (getExpressionPrecedence(replacement) < getExpressionPrecedence(parent) || needsParentheses(parent))) {
|
||||
return factory.createParenthesizedExpression(replacement);
|
||||
}
|
||||
|
||||
// Functions also need to be parenthesized.
|
||||
// E.g.: const f = () => {}; f(); -> (() => {})();
|
||||
if (isFunctionLike(replacement) && isCallLikeExpression(parent)) {
|
||||
return factory.createParenthesizedExpression(replacement);
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
18
tests/cases/fourslash/inlineVariableExportedVariable.ts
Normal file
18
tests/cases/fourslash/inlineVariableExportedVariable.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////export const /*a1*/x/*b1*/ = 1;
|
||||
////const /*a2*/y/*b2*/ = 2;
|
||||
////const /*a3*/z/*b3*/ = 3;
|
||||
////const u = x + 1;
|
||||
////const v = 2 + y;
|
||||
////export { y };
|
||||
////export { z as w };
|
||||
|
||||
goTo.select("a1", "b1");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
|
||||
goTo.select("a2", "b2");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
|
||||
goTo.select("a3", "b3");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
17
tests/cases/fourslash/inlineVariableFunctionResult.ts
Normal file
17
tests/cases/fourslash/inlineVariableFunctionResult.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////function inc(x: number) { return x + 1; }
|
||||
////const /*a*/y/*b*/ = inc(1);
|
||||
////const z = y + 1;
|
||||
////const w = inc(y);
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: `function inc(x: number) { return x + 1; }
|
||||
const z = inc(1) + 1;
|
||||
const w = inc(inc(1));`
|
||||
});
|
||||
35
tests/cases/fourslash/inlineVariableJsxCallback.ts
Normal file
35
tests/cases/fourslash/inlineVariableJsxCallback.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
//@module: commonjs
|
||||
//@jsx: preserve
|
||||
|
||||
// @Filename: file.tsx
|
||||
////function Button() {
|
||||
//// const /*a*/onClick/*b*/ = () => {
|
||||
//// console.log("clicked");
|
||||
//// };
|
||||
////
|
||||
//// return (
|
||||
//// <button onClick={onClick}>
|
||||
//// Click me!
|
||||
//// </button>
|
||||
//// );
|
||||
////}
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: `function Button() {
|
||||
|
||||
return (
|
||||
<button onClick={() => {
|
||||
console.log("clicked");
|
||||
}}>
|
||||
Click me!
|
||||
</button>
|
||||
);
|
||||
}`
|
||||
});
|
||||
11
tests/cases/fourslash/inlineVariableMultipleDeclarations.ts
Normal file
11
tests/cases/fourslash/inlineVariableMultipleDeclarations.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a1*/foo/*b1*/ = "foo";
|
||||
////type /*a2*/foo/*b2*/ = string;
|
||||
////type bar = foo;
|
||||
|
||||
goTo.select("a1", "b1");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
|
||||
goTo.select("a2", "b2");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
19
tests/cases/fourslash/inlineVariableMultipleScopes.ts
Normal file
19
tests/cases/fourslash/inlineVariableMultipleScopes.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////let /*a*/x/*b*/ = 1;
|
||||
////function foo() {
|
||||
//// console.log(x);
|
||||
////}
|
||||
////const y = x + 2;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: `function foo() {
|
||||
console.log(1);
|
||||
}
|
||||
const y = 1 + 2;`
|
||||
});
|
||||
27
tests/cases/fourslash/inlineVariableNeedsParens.ts
Normal file
27
tests/cases/fourslash/inlineVariableNeedsParens.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a1*/x/*b1*/ = 1 + 2;
|
||||
////const y = x * 3;
|
||||
////const /*a2*/f/*b2*/ = () => { };
|
||||
////f();
|
||||
|
||||
goTo.select("a1", "b1");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: `const y = (1 + 2) * 3;
|
||||
const f = () => { };
|
||||
f();`
|
||||
});
|
||||
|
||||
goTo.select("a2", "b2");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: `const y = (1 + 2) * 3;
|
||||
(() => { })();`
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////let /*a*/x/*b*/;
|
||||
////const y = x + 1;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
@ -0,0 +1,8 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////for (let /*a*/i/*b*/ = 0; i < 5; i++) {
|
||||
//// console.log(i)
|
||||
////}
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
15
tests/cases/fourslash/inlineVariableNullCoalescing.ts
Normal file
15
tests/cases/fourslash/inlineVariableNullCoalescing.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////function foo(): number | undefined { return Math.random() > 0.5 ? 1 : undefined; }
|
||||
////const /*a*/x/*b*/ = foo();
|
||||
////const y = x?.toString();
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: `function foo(): number | undefined { return Math.random() > 0.5 ? 1 : undefined; }
|
||||
const y = (foo())?.toString();`
|
||||
});
|
||||
7
tests/cases/fourslash/inlineVariableOnlyDeclaration.ts
Normal file
7
tests/cases/fourslash/inlineVariableOnlyDeclaration.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a*/x/*b*/ = 0;
|
||||
////const y = 1 + 2;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
@ -0,0 +1,6 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a*/foo/*b*/ = () => foo();
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
16
tests/cases/fourslash/inlineVariableRefactorAvailability.ts
Normal file
16
tests/cases/fourslash/inlineVariableRefactorAvailability.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const/*a*/ /*b*/x/*c*/ = /*d*/1;
|
||||
////const y = x + 2;
|
||||
|
||||
goTo.marker("a");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
|
||||
goTo.marker("b");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
|
||||
goTo.marker("c");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
|
||||
goTo.marker("d");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
13
tests/cases/fourslash/inlineVariableSingleReference.ts
Normal file
13
tests/cases/fourslash/inlineVariableSingleReference.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a*/x/*b*/ = 0;
|
||||
////const y = x + 1;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorAvailable("Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: "const y = 0 + 1;"
|
||||
});
|
||||
15
tests/cases/fourslash/inlineVariableTriggerInReference.ts
Normal file
15
tests/cases/fourslash/inlineVariableTriggerInReference.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const x = 0;
|
||||
////const y = /*a*/x/*b*/ + 1;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailableForTriggerReason("implicit", "Inline variable");
|
||||
verify.refactorAvailableForTriggerReason("invoked", "Inline variable");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Inline variable",
|
||||
actionName: "Inline variable",
|
||||
actionDescription: "Inline variable",
|
||||
newContent: "const y = 0 + 1;",
|
||||
triggerReason: "invoked"
|
||||
});
|
||||
7
tests/cases/fourslash/inlineVariableTypeof.ts
Normal file
7
tests/cases/fourslash/inlineVariableTypeof.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a*/Foo/*b*/ = class Foo {}
|
||||
////type FooConstructor = typeof Foo;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
7
tests/cases/fourslash/inlineVariableWriteReference.ts
Normal file
7
tests/cases/fourslash/inlineVariableWriteReference.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
////const /*a*/x/*b*/ = 0;
|
||||
////const y = x++ + 1;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.not.refactorAvailable("Inline variable");
|
||||
Loading…
x
Reference in New Issue
Block a user