Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
75 changes: 72 additions & 3 deletions src/services/refactors/convertParamsToDestructuredObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,23 @@ namespace ts.refactor.convertParamsToDestructuredObject {
changes: textChanges.ChangeTracker,
functionDeclaration: ValidFunctionDeclaration,
groupedReferences: GroupedReferences): void {
const newParamDeclaration = map(createNewParameters(functionDeclaration, program, host), param => getSynthesizedDeepClone(param));
const signature = groupedReferences.signature;
const newParamDeclaration = map(createNewParameters(signature ?? functionDeclaration, program, host), param => getSynthesizedDeepClone(param));

if (signature) {
changes.replaceNodeRangeWithNodes(
sourceFile,
first(signature.parameters),
last(signature.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
});
}

changes.replaceNodeRangeWithNodes(
sourceFile,
first(functionDeclaration.parameters),
Expand Down Expand Up @@ -99,13 +115,38 @@ namespace ts.refactor.convertParamsToDestructuredObject {
const functionSymbols = map(functionNames, getSymbolTargetAtLocation);
const classSymbols = map(classNames, getSymbolTargetAtLocation);
const isConstructor = isConstructorDeclaration(functionDeclaration);
const interfaceSymbols = map(functionNames, getSymbolForInterfaceSignature);

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

/* Calls and declarations of implemented interface methods should point to the symbol of the method signature */
if (contains(interfaceSymbols, 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;
}
}

/* Declarations in interface implementations have their own symbol so we need to go to the interface declaration
to get the type from the method signature. */
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand which part gets the type from the method signature. Is it entryToDeclaration? getSymbolForInterfaceSignature only gets the symbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed comment.

const interfaceSymbol = getSymbolForInterfaceSignature(entry.node);
if (interfaceSymbol && contains(interfaceSymbols, interfaceSymbol)) {
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 @@ -173,6 +214,21 @@ namespace ts.refactor.convertParamsToDestructuredObject {
const symbol = checker.getSymbolAtLocation(node);
return symbol && getSymbolTarget(symbol, checker);
}

function getSymbolForInterfaceSignature(node: Node): Symbol | undefined {
const element = getContainingObjectLiteralElement(node);
if (element) {
const contextualType = element && checker.getContextualType(element.parent);
if (contextualType) {
const name = getNameFromPropertyName(element.name);
if (!name) return;
if (!contextualType.isUnion()) {
return contextualType.getProperty(name);
}
}
}
return undefined;
}
}

function entryToImportOrExport(entry: FindAllReferences.NodeEntry): Node | undefined {
Expand Down Expand Up @@ -292,6 +348,10 @@ namespace ts.refactor.convertParamsToDestructuredObject {
return false;
}

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

function isValidFunctionDeclaration(
functionDeclaration: FunctionLikeDeclaration,
checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration {
Expand All @@ -300,6 +360,10 @@ namespace ts.refactor.convertParamsToDestructuredObject {
case SyntaxKind.FunctionDeclaration:
return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker);
case SyntaxKind.MethodDeclaration:
if (isObjectLiteralExpression(functionDeclaration.parent)) {
const contextualType = checker.getContextualType(functionDeclaration.parent);
return !contextualType?.isUnion() && isSingleImplementation(functionDeclaration, checker);
}
return isSingleImplementation(functionDeclaration, checker);
case SyntaxKind.Constructor:
if (isClassDeclaration(functionDeclaration.parent)) {
Expand Down Expand Up @@ -398,7 +462,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 +648,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 +663,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,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");