Skip to content

Refactor params for interface #42652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 80 additions & 14 deletions src/services/refactors/convertParamsToDestructuredObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,14 @@ namespace ts.refactor.convertParamsToDestructuredObject {
changes: textChanges.ChangeTracker,
functionDeclaration: ValidFunctionDeclaration,
groupedReferences: GroupedReferences): void {
const newParamDeclaration = map(createNewParameters(functionDeclaration, program, host), param => getSynthesizedDeepClone(param));
changes.replaceNodeRangeWithNodes(
sourceFile,
first(functionDeclaration.parameters),
last(functionDeclaration.parameters),
newParamDeclaration,
{ joiner: ", ",
// indentation is set to 0 because otherwise the object parameter will be indented if there is a `this` parameter
indentation: 0,
leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll,
trailingTriviaOption: textChanges.TrailingTriviaOption.Include
});
const signature = groupedReferences.signature;
const newFunctionDeclarationParams = map(createNewParameters(functionDeclaration, program, host), param => getSynthesizedDeepClone(param));

if (signature) {
const newSignatureParams = map(createNewParameters(signature, program, host), param => getSynthesizedDeepClone(param));
replaceParameters(signature, newSignatureParams);
}
replaceParameters(functionDeclaration, newFunctionDeclarationParams);
Comment on lines +57 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these branches be mutually exclusive? This looks suspicious to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the signature is defined, that means we were able to connect the method declaration back to it. That is, functionDeclaration implements signature and both should be changed. The absence of signature simply means functionDeclaration does not implement a method declared elsewhere (that we know of).


const functionCalls = sortAndDeduplicate(groupedReferences.functionCalls, /*comparer*/ (a, b) => compareValues(a.pos, b.pos));
for (const call of functionCalls) {
Expand All @@ -76,6 +72,20 @@ namespace ts.refactor.convertParamsToDestructuredObject {
{ leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll, trailingTriviaOption: textChanges.TrailingTriviaOption.Include });
}
}

function replaceParameters(declarationOrSignature: ValidFunctionDeclaration | ValidMethodSignature, parameterDeclarations: ParameterDeclaration[]) {
changes.replaceNodeRangeWithNodes(
sourceFile,
first(declarationOrSignature.parameters),
last(declarationOrSignature.parameters),
parameterDeclarations,
{ joiner: ", ",
// indentation is set to 0 because otherwise the object parameter will be indented if there is a `this` parameter
indentation: 0,
leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll,
trailingTriviaOption: textChanges.TrailingTriviaOption.Include
});
}
}

function getGroupedReferences(functionDeclaration: ValidFunctionDeclaration, program: Program, cancellationToken: CancellationToken): GroupedReferences {
Expand All @@ -99,13 +109,41 @@ namespace ts.refactor.convertParamsToDestructuredObject {
const functionSymbols = map(functionNames, getSymbolTargetAtLocation);
const classSymbols = map(classNames, getSymbolTargetAtLocation);
const isConstructor = isConstructorDeclaration(functionDeclaration);
const contextualSymbols = map(functionNames, (name) => (getSymbolForContextualType(name, checker)));

for (const entry of referenceEntries) {
if (entry.kind !== FindAllReferences.EntryKind.Node) {
if (entry.kind === FindAllReferences.EntryKind.Span) {
groupedReferences.valid = false;
continue;
}

/* Declarations in object literals may be implementations of method signatures which have a different symbol from the declaration
For example:
interface IFoo { m(a: number): void }
const foo: IFoo = { m(a: number): void {} }
In these cases we get the symbol for the signature from the contextual type.
*/
if (contains(contextualSymbols, getSymbolTargetAtLocation(entry.node))) {
if (isValidMethodSignature(entry.node.parent)) {
groupedReferences.signature = entry.node.parent;
continue;
}
const call = entryToFunctionCall(entry);
if (call) {
groupedReferences.functionCalls.push(call);
continue;
}
}

const contextualSymbol = getSymbolForContextualType(entry.node, checker);
if (contextualSymbol && contains(contextualSymbols, contextualSymbol)) {
const decl = entryToDeclaration(entry);
if (decl) {
groupedReferences.declarations.push(decl);
continue;
}
}

/* We compare symbols because in some cases find all references wil return a reference that may or may not be to the refactored function.
Example from the refactorConvertParamsToDestructuredObject_methodCallUnion.ts test:
class A { foo(a: number, b: number) { return a + b; } }
Expand Down Expand Up @@ -175,6 +213,20 @@ namespace ts.refactor.convertParamsToDestructuredObject {
}
}

/**
* Gets the symbol for the contextual type of the node if it is not a union or intersection.
*/
function getSymbolForContextualType(node: Node, checker: TypeChecker): Symbol | undefined {
const element = getContainingObjectLiteralElement(node);
if (element) {
const contextualType = checker.getContextualTypeForObjectLiteralElement(<ObjectLiteralElementLike>element);
const symbol = contextualType?.getSymbol();
if (symbol && !(getCheckFlags(symbol) & CheckFlags.Synthetic)) {
return symbol;
}
}
}

function entryToImportOrExport(entry: FindAllReferences.NodeEntry): Node | undefined {
const node = entry.node;

Expand Down Expand Up @@ -292,6 +344,10 @@ namespace ts.refactor.convertParamsToDestructuredObject {
return false;
}

function isValidMethodSignature(node: Node): node is ValidMethodSignature {
return isMethodSignature(node) && (isInterfaceDeclaration(node.parent) || isTypeLiteralNode(node.parent));
}

function isValidFunctionDeclaration(
functionDeclaration: FunctionLikeDeclaration,
checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration {
Expand All @@ -300,6 +356,11 @@ namespace ts.refactor.convertParamsToDestructuredObject {
case SyntaxKind.FunctionDeclaration:
return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker);
case SyntaxKind.MethodDeclaration:
if (isObjectLiteralExpression(functionDeclaration.parent)) {
const contextualSymbol = getSymbolForContextualType(functionDeclaration.name, checker);
// don't offer the refactor when there are multiple signatures since we won't know which ones the user wants to change
return contextualSymbol?.declarations.length === 1 && isSingleImplementation(functionDeclaration, checker);
}
return isSingleImplementation(functionDeclaration, checker);
case SyntaxKind.Constructor:
if (isClassDeclaration(functionDeclaration.parent)) {
Expand Down Expand Up @@ -398,7 +459,7 @@ namespace ts.refactor.convertParamsToDestructuredObject {
return objectLiteral;
}

function createNewParameters(functionDeclaration: ValidFunctionDeclaration, program: Program, host: LanguageServiceHost): NodeArray<ParameterDeclaration> {
function createNewParameters(functionDeclaration: ValidFunctionDeclaration | ValidMethodSignature, program: Program, host: LanguageServiceHost): NodeArray<ParameterDeclaration> {
const checker = program.getTypeChecker();
const refactorableParameters = getRefactorableParameters(functionDeclaration.parameters);
const bindingElements = map(refactorableParameters, createBindingElementFromParameterDeclaration);
Expand Down Expand Up @@ -584,6 +645,10 @@ namespace ts.refactor.convertParamsToDestructuredObject {
parameters: NodeArray<ValidParameterDeclaration>;
}

interface ValidMethodSignature extends MethodSignature {
parameters: NodeArray<ValidParameterDeclaration>;
}

type ValidFunctionDeclaration = ValidConstructor | ValidFunction | ValidMethod | ValidArrowFunction | ValidFunctionExpression;

interface ValidParameterDeclaration extends ParameterDeclaration {
Expand All @@ -595,6 +660,7 @@ namespace ts.refactor.convertParamsToDestructuredObject {
interface GroupedReferences {
functionCalls: (CallExpression | NewExpression)[];
declarations: Node[];
signature?: ValidMethodSignature;
classReferences?: ClassReferences;
valid: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />

////interface IFoo {
//// method(x: string, y: string): void;
////}
////const x: IFoo = {
//// method(/*a*/x: string, y: string/*b*/): void {},
////};

goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `interface IFoo {
method({ x, y }: { x: string; y: string; }): void;
}
const x: IFoo = {
method({ x, y }: { x: string; y: string; }): void {},
};`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />

////interface IFoo {
//// method(x: string, y: string): void;
////}
////const x: IFoo = {
//// method(/*a*/x: string, y: string, z?: string/*b*/): void {},
////};

goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `interface IFoo {
method({ x, y }: { x: string; y: string; }): void;
}
const x: IFoo = {
method({ x, y, z }: { x: string; y: string; z?: string; }): void {},
};`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// <reference path='fourslash.ts' />

////interface IFoo {
//// method(x: string, y: string): void;
////}
////const x: IFoo = {
//// method(/*a*/x, y/*b*/): void {},
////};

/*
When there are no type annotations on the params in the implementation, we ultimately
would like to handle them like we do for calls resulting in `method({x, y}) {}`.

Note that simply adding the annotations from the signature can fail as the implementation
can take more paramters than the signatures.
*/
goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `interface IFoo {
method({ x, y }: { x: string; y: string; }): void;
}
const x: IFoo = {
method({ x, y }: { x; y; }): void {},
};`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

////interface IFoo {
//// method(x: string, y: string): void;
//// method(x: number, y: string): void;
////}
////const x: IFoo = {
//// method(/*a*/x: string | number, y: string/*b*/): void {},
////};

// For multiple signatures, we don't have a reliable way to determine
// which signature to match to or if all signatures should be changed.
goTo.select("a", "b");
verify.not.refactorAvailable("Convert parameters to destructured object");
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

////interface IFoo {
//// method(x: string, y: string): void;
////}
////interface IBar {
//// method(x: number): void;
////}
////const x: IFoo & IBar = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens with unions? Do we even get the error that triggers the codefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a test refactorConvertParamsToDestructuredObject_interfaceNoUnion.ts:
https://github.com/microsoft/TypeScript/pull/42652/files#diff-bb37f20a7f9bacc80fda3468d1e6c808314c79f47e8f58dc3d55c7e3d5447360

It's a refactor so we just don't offer it.

//// method(/*a*/x: string, y: string/*b*/): void {},
////};

goTo.select("a", "b");
verify.not.refactorAvailable("Convert parameters to destructured object");
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

////interface IFoo {
//// method(x: string, y: string): void;
////}
////interface IBar {
//// method(x: number): void;
////}
////const x: IFoo | IBar = {
//// method(/*a*/x: string, y: string/*b*/): void {},
////};

goTo.select("a", "b");
verify.not.refactorAvailable("Convert parameters to destructured object");
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />

////type Foo = {
//// method(x: string, y: string): void;
////}
////const x: Foo = {
//// method(/*a*/x: string, y: string/*b*/): void {},
////};

goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Convert parameters to destructured object",
actionName: "Convert parameters to destructured object",
actionDescription: "Convert parameters to destructured object",
newContent: `type Foo = {
method({ x, y }: { x: string; y: string; }): void;
}
const x: Foo = {
method({ x, y }: { x: string; y: string; }): void {},
};`
});