Add inline variable refactor (#54281)

This commit is contained in:
Maria José Solano 2023-06-12 20:03:43 -07:00 committed by GitHub
parent 89cbea8e16
commit 31936ea36e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 483 additions and 2 deletions

View File

@ -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",

View File

@ -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";

View File

@ -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);
}

View 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;
}

View 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");

View 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));`
});

View 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>
);
}`
});

View 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");

View 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;`
});

View 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;
(() => { })();`
});

View File

@ -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");

View File

@ -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");

View 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();`
});

View 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");

View File

@ -0,0 +1,6 @@
/// <reference path="fourslash.ts" />
////const /*a*/foo/*b*/ = () => foo();
goTo.select("a", "b");
verify.not.refactorAvailable("Inline variable");

View 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");

View 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;"
});

View 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"
});

View 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");

View 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");