Skip to content

Commit 10b9051

Browse files
authored
Fix convert to named parameters rest parameter tuple (#30286)
* check if rest parameter is of tuple type in isOptionalParameter * expose isArrayType and isTupleType in checker * don't offer refactor if rest parameter type is neither array nor tuple type * add tests for rest parameters * fix tests for renamed refactor * remove unnecessary conditional operator
1 parent d8591a8 commit 10b9051

5 files changed

+131
-26
lines changed

src/compiler/checker.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ namespace ts {
305305
getNeverType: () => neverType,
306306
isSymbolAccessible,
307307
getObjectFlags,
308+
isArrayType,
309+
isTupleType,
308310
isArrayLikeType,
309311
isTypeInvalidDueToUnionDiscriminant,
310312
getAllPossiblePropertiesOfTypes,

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3252,6 +3252,8 @@ namespace ts {
32523252
/* @internal */ getSymbolCount(): number;
32533253
/* @internal */ getTypeCount(): number;
32543254

3255+
/* @internal */ isArrayType(type: Type): boolean;
3256+
/* @internal */ isTupleType(type: Type): boolean;
32553257
/* @internal */ isArrayLikeType(type: Type): boolean;
32563258
/* @internal */ getObjectFlags(type: Type): ObjectFlags;
32573259

src/services/refactors/convertParamsToDestructuredObject.ts

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ namespace ts.refactor.convertParamsToDestructuredObject {
8181
const references = flatMap(names, /*mapfn*/ name => FindAllReferences.getReferenceEntriesForNode(-1, name, program, program.getSourceFiles(), cancellationToken));
8282
const groupedReferences = groupReferences(references);
8383

84-
if (!every(groupedReferences.declarations, decl => contains(names, decl))) {
84+
if (!every(groupedReferences.declarations, /*callback*/ decl => contains(names, decl))) {
8585
groupedReferences.valid = false;
8686
}
8787

@@ -241,18 +241,26 @@ namespace ts.refactor.convertParamsToDestructuredObject {
241241
return undefined;
242242
}
243243

244-
function isValidFunctionDeclaration(functionDeclaration: SignatureDeclaration, checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration {
245-
if (!isValidParameterNodeArray(functionDeclaration.parameters)) return false;
244+
function isValidFunctionDeclaration(
245+
functionDeclaration: SignatureDeclaration,
246+
checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration {
247+
if (!isValidParameterNodeArray(functionDeclaration.parameters, checker)) return false;
246248
switch (functionDeclaration.kind) {
247249
case SyntaxKind.FunctionDeclaration:
248250
case SyntaxKind.MethodDeclaration:
249-
return !!functionDeclaration.name && !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration);
251+
return !!functionDeclaration.name
252+
&& !!functionDeclaration.body
253+
&& !checker.isImplementationOfOverload(functionDeclaration);
250254
case SyntaxKind.Constructor:
251255
if (isClassDeclaration(functionDeclaration.parent)) {
252-
return !!functionDeclaration.body && !!functionDeclaration.parent.name && !checker.isImplementationOfOverload(functionDeclaration);
256+
return !!functionDeclaration.body
257+
&& !!functionDeclaration.parent.name
258+
&& !checker.isImplementationOfOverload(functionDeclaration);
253259
}
254260
else {
255-
return isValidVariableDeclaration(functionDeclaration.parent.parent) && !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration);
261+
return isValidVariableDeclaration(functionDeclaration.parent.parent)
262+
&& !!functionDeclaration.body
263+
&& !checker.isImplementationOfOverload(functionDeclaration);
256264
}
257265
case SyntaxKind.FunctionExpression:
258266
case SyntaxKind.ArrowFunction:
@@ -261,12 +269,21 @@ namespace ts.refactor.convertParamsToDestructuredObject {
261269
return false;
262270
}
263271

264-
function isValidParameterNodeArray(parameters: NodeArray<ParameterDeclaration>): parameters is ValidParameterNodeArray {
265-
return getRefactorableParametersLength(parameters) >= minimumParameterLength && every(parameters, isValidParameterDeclaration);
272+
function isValidParameterNodeArray(
273+
parameters: NodeArray<ParameterDeclaration>,
274+
checker: TypeChecker): parameters is ValidParameterNodeArray {
275+
return getRefactorableParametersLength(parameters) >= minimumParameterLength
276+
&& every(parameters, /*callback*/ paramDecl => isValidParameterDeclaration(paramDecl, checker));
266277
}
267278

268-
function isValidParameterDeclaration(paramDeclaration: ParameterDeclaration): paramDeclaration is ValidParameterDeclaration {
269-
return !paramDeclaration.modifiers && !paramDeclaration.decorators && isIdentifier(paramDeclaration.name);
279+
function isValidParameterDeclaration(
280+
parameterDeclaration: ParameterDeclaration,
281+
checker: TypeChecker): parameterDeclaration is ValidParameterDeclaration {
282+
if (isRestParameter(parameterDeclaration)) {
283+
const type = checker.getTypeAtLocation(parameterDeclaration);
284+
if (!checker.isArrayType(type) && !checker.isTupleType(type)) return false;
285+
}
286+
return !parameterDeclaration.modifiers && !parameterDeclaration.decorators && isIdentifier(parameterDeclaration.name);
270287
}
271288

272289
function isValidVariableDeclaration(node: Node): node is ValidVariableDeclaration {
@@ -313,15 +330,15 @@ namespace ts.refactor.convertParamsToDestructuredObject {
313330
}
314331

315332
function createNewParameters(functionDeclaration: ValidFunctionDeclaration, program: Program, host: LanguageServiceHost): NodeArray<ParameterDeclaration> {
333+
const checker = program.getTypeChecker();
316334
const refactorableParameters = getRefactorableParameters(functionDeclaration.parameters);
317335
const bindingElements = map(refactorableParameters, createBindingElementFromParameterDeclaration);
318336
const objectParameterName = createObjectBindingPattern(bindingElements);
319337
const objectParameterType = createParameterTypeNode(refactorableParameters);
320-
const checker = program.getTypeChecker();
321338

322339
let objectInitializer: Expression | undefined;
323340
// If every parameter in the original function was optional, add an empty object initializer to the new object parameter
324-
if (every(refactorableParameters, checker.isOptionalParameter)) {
341+
if (every(refactorableParameters, isOptionalParameter)) {
325342
objectInitializer = createObjectLiteral();
326343
}
327344

@@ -355,6 +372,20 @@ namespace ts.refactor.convertParamsToDestructuredObject {
355372
}
356373
return createNodeArray([objectParameter]);
357374

375+
function createBindingElementFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): BindingElement {
376+
const element = createBindingElement(
377+
/*dotDotDotToken*/ undefined,
378+
/*propertyName*/ undefined,
379+
getParameterName(parameterDeclaration),
380+
isRestParameter(parameterDeclaration) && isOptionalParameter(parameterDeclaration) ? createArrayLiteral() : parameterDeclaration.initializer);
381+
382+
suppressLeadingAndTrailingTrivia(element);
383+
if (parameterDeclaration.initializer && element.initializer) {
384+
copyComments(parameterDeclaration.initializer, element.initializer);
385+
}
386+
return element;
387+
}
388+
358389
function createParameterTypeNode(parameters: NodeArray<ValidParameterDeclaration>): TypeLiteralNode {
359390
const members = map(parameters, createPropertySignatureFromParameterDeclaration);
360391
const typeNode = addEmitFlags(createTypeLiteralNode(members), EmitFlags.SingleLine);
@@ -370,7 +401,7 @@ namespace ts.refactor.convertParamsToDestructuredObject {
370401
const propertySignature = createPropertySignature(
371402
/*modifiers*/ undefined,
372403
getParameterName(parameterDeclaration),
373-
parameterDeclaration.initializer || isRestParameter(parameterDeclaration) ? createToken(SyntaxKind.QuestionToken) : parameterDeclaration.questionToken,
404+
isOptionalParameter(parameterDeclaration) ? createToken(SyntaxKind.QuestionToken) : parameterDeclaration.questionToken,
374405
parameterType,
375406
/*initializer*/ undefined);
376407

@@ -384,24 +415,17 @@ namespace ts.refactor.convertParamsToDestructuredObject {
384415
}
385416

386417
function getTypeNode(node: Node): TypeNode | undefined {
387-
const checker = program.getTypeChecker();
388418
const type = checker.getTypeAtLocation(node);
389419
return getTypeNodeIfAccessible(type, node, program, host);
390420
}
391-
}
392421

393-
function createBindingElementFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): BindingElement {
394-
const element = createBindingElement(
395-
/*dotDotDotToken*/ undefined,
396-
/*propertyName*/ undefined,
397-
getParameterName(parameterDeclaration),
398-
isRestParameter(parameterDeclaration) ? createArrayLiteral() : parameterDeclaration.initializer);
399-
400-
suppressLeadingAndTrailingTrivia(element);
401-
if (parameterDeclaration.initializer && element.initializer) {
402-
copyComments(parameterDeclaration.initializer, element.initializer);
422+
function isOptionalParameter(parameterDeclaration: ValidParameterDeclaration): boolean {
423+
if (isRestParameter(parameterDeclaration)) {
424+
const type = checker.getTypeAtLocation(parameterDeclaration);
425+
return !checker.isTupleType(type);
426+
}
427+
return checker.isOptionalParameter(parameterDeclaration);
403428
}
404-
return element;
405429
}
406430

407431
function copyComments(sourceNode: Node, targetNode: Node) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: a.ts
4+
////function /*a*/gen<T extends any[]>/*b*/(a: boolean, ...b: T): T {
5+
//// return b;
6+
////}
7+
////gen(false);
8+
////gen(false, 1);
9+
////gen(true, 1, "two");
10+
11+
// @Filename: b.ts
12+
////function /*c*/un/*d*/(a: boolean, ...b: number[] | [string, string]) {
13+
//// return b;
14+
////}
15+
16+
goTo.select("a", "b");
17+
verify.not.refactorAvailable("Convert to named parameters");
18+
19+
goTo.select("c", "d");
20+
verify.not.refactorAvailable("Convert to named parameters");
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: a.ts
4+
////function /*a*/fn1/*b*/(a: number, b: number, ...args: [number, number]) { }
5+
////fn1(1, 2, 3, 4);
6+
7+
// @Filename: b.ts
8+
////function /*c*/fn2/*d*/(a: number, b: number, ...args: [number, number, ...string[]]) { }
9+
////fn2(1, 2, 3, 4);
10+
////fn2(1, 2, 3, 4, "a");
11+
12+
// @Filename: c.ts
13+
////function /*e*/fn3/*f*/(b: boolean, c: []) { }
14+
////fn3(true, []);
15+
16+
// @Filename: d.ts
17+
////function /*g*/fn4/*h*/(a: number, ...args: [...string[]] ) { }
18+
////fn4(2);
19+
////fn4(1, "two", "three");
20+
21+
goTo.select("a", "b");
22+
edit.applyRefactor({
23+
refactorName: "Convert parameters to destructured object",
24+
actionName: "Convert parameters to destructured object",
25+
actionDescription: "Convert parameters to destructured object",
26+
newContent: `function fn1({ a, b, args }: { a: number; b: number; args: [number, number]; }) { }
27+
fn1({ a: 1, b: 2, args: [3, 4] });`
28+
});
29+
30+
goTo.select("c", "d");
31+
edit.applyRefactor({
32+
refactorName: "Convert parameters to destructured object",
33+
actionName: "Convert parameters to destructured object",
34+
actionDescription: "Convert parameters to destructured object",
35+
newContent: `function fn2({ a, b, args }: { a: number; b: number; args: [number, number, ...string[]]; }) { }
36+
fn2({ a: 1, b: 2, args: [3, 4] });
37+
fn2({ a: 1, b: 2, args: [3, 4, "a"] });`
38+
});
39+
40+
goTo.select("e", "f");
41+
edit.applyRefactor({
42+
refactorName: "Convert parameters to destructured object",
43+
actionName: "Convert parameters to destructured object",
44+
actionDescription: "Convert parameters to destructured object",
45+
newContent: `function fn3({ b, c }: { b: boolean; c: []; }) { }
46+
fn3({ b: true, c: [] });`
47+
});
48+
49+
goTo.select("g", "h");
50+
edit.applyRefactor({
51+
refactorName: "Convert parameters to destructured object",
52+
actionName: "Convert parameters to destructured object",
53+
actionDescription: "Convert parameters to destructured object",
54+
newContent: `function fn4({ a, args = [] }: { a: number; args?: [...string[]]; }) { }
55+
fn4({ a: 2 });
56+
fn4({ a: 1, args: ["two", "three"] });`
57+
});

0 commit comments

Comments
 (0)