Skip to content

Commit 140fa7e

Browse files
authored
feat(51870): Add a Quick Fix to add an additional parameter to a method or function (#56411)
1 parent 55153b0 commit 140fa7e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+906
-2
lines changed

src/compiler/diagnosticMessages.json

+24
Original file line numberDiff line numberDiff line change
@@ -7792,6 +7792,30 @@
77927792
"category": "Message",
77937793
"code": 95187
77947794
},
7795+
"Add missing parameter to '{0}'": {
7796+
"category": "Message",
7797+
"code": 95188
7798+
},
7799+
"Add missing parameters to '{0}'": {
7800+
"category": "Message",
7801+
"code": 95189
7802+
},
7803+
"Add all missing parameters": {
7804+
"category": "Message",
7805+
"code": 95190
7806+
},
7807+
"Add optional parameter to '{0}'": {
7808+
"category": "Message",
7809+
"code": 95191
7810+
},
7811+
"Add optional parameters to '{0}'": {
7812+
"category": "Message",
7813+
"code": 95192
7814+
},
7815+
"Add all optional parameters": {
7816+
"category": "Message",
7817+
"code": 95193
7818+
},
77957819

77967820
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
77977821
"category": "Error",

src/services/_namespaces/ts.codefix.ts

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export * from "../codefixes/fixSpelling";
3131
export * from "../codefixes/returnValueCorrect";
3232
export * from "../codefixes/fixAddMissingMember";
3333
export * from "../codefixes/fixAddMissingNewOperator";
34+
export * from "../codefixes/fixAddMissingParam";
3435
export * from "../codefixes/fixCannotFindModule";
3536
export * from "../codefixes/fixClassDoesntImplementInheritedAbstractMember";
3637
export * from "../codefixes/fixClassSuperMustPrecedeThisAccess";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import {
2+
append,
3+
ArrowFunction,
4+
CodeFixAction,
5+
declarationNameToString,
6+
Diagnostics,
7+
factory,
8+
filter,
9+
findAncestor,
10+
first,
11+
forEach,
12+
FunctionDeclaration,
13+
FunctionExpression,
14+
FunctionLikeDeclaration,
15+
getNameOfAccessExpression,
16+
getNameOfDeclaration,
17+
getTokenAtPosition,
18+
isAccessExpression,
19+
isCallExpression,
20+
isIdentifier,
21+
isParameter,
22+
isPropertyDeclaration,
23+
isSourceFileFromLibrary,
24+
isVariableDeclaration,
25+
last,
26+
lastOrUndefined,
27+
length,
28+
map,
29+
MethodDeclaration,
30+
Node,
31+
NodeBuilderFlags,
32+
ParameterDeclaration,
33+
Program,
34+
QuestionToken,
35+
some,
36+
SourceFile,
37+
SyntaxKind,
38+
textChanges,
39+
Type,
40+
TypeChecker,
41+
TypeNode,
42+
} from "../_namespaces/ts";
43+
import {
44+
codeFixAll,
45+
createCodeFixAction,
46+
registerCodeFix,
47+
} from "../_namespaces/ts.codefix";
48+
49+
const addMissingParamFixId = "addMissingParam";
50+
const addOptionalParamFixId = "addOptionalParam";
51+
const errorCodes = [Diagnostics.Expected_0_arguments_but_got_1.code];
52+
53+
registerCodeFix({
54+
errorCodes,
55+
fixIds: [addMissingParamFixId, addOptionalParamFixId],
56+
getCodeActions(context) {
57+
const info = getInfo(context.sourceFile, context.program, context.span.start);
58+
if (info === undefined) return undefined;
59+
60+
const { name, declarations, newParameters, newOptionalParameters } = info;
61+
const actions: CodeFixAction[] = [];
62+
63+
if (length(newParameters)) {
64+
append(
65+
actions,
66+
createCodeFixAction(
67+
addMissingParamFixId,
68+
textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, declarations, newParameters)),
69+
[length(newParameters) > 1 ? Diagnostics.Add_missing_parameters_to_0 : Diagnostics.Add_missing_parameter_to_0, name],
70+
addMissingParamFixId,
71+
Diagnostics.Add_all_missing_parameters,
72+
),
73+
);
74+
}
75+
76+
if (length(newOptionalParameters)) {
77+
append(
78+
actions,
79+
createCodeFixAction(
80+
addOptionalParamFixId,
81+
textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, declarations, newOptionalParameters)),
82+
[length(newOptionalParameters) > 1 ? Diagnostics.Add_optional_parameters_to_0 : Diagnostics.Add_optional_parameter_to_0, name],
83+
addOptionalParamFixId,
84+
Diagnostics.Add_all_optional_parameters,
85+
),
86+
);
87+
}
88+
89+
return actions;
90+
},
91+
getAllCodeActions: context =>
92+
codeFixAll(context, errorCodes, (changes, diag) => {
93+
const info = getInfo(context.sourceFile, context.program, diag.start);
94+
if (info) {
95+
const { declarations, newParameters, newOptionalParameters } = info;
96+
if (context.fixId === addMissingParamFixId) {
97+
doChange(changes, context.sourceFile, declarations, newParameters);
98+
}
99+
if (context.fixId === addOptionalParamFixId) {
100+
doChange(changes, context.sourceFile, declarations, newOptionalParameters);
101+
}
102+
}
103+
}),
104+
});
105+
106+
type ConvertibleSignatureDeclaration =
107+
| FunctionDeclaration
108+
| FunctionExpression
109+
| ArrowFunction
110+
| MethodDeclaration;
111+
112+
interface SignatureInfo {
113+
readonly newParameters: ParameterInfo[];
114+
readonly newOptionalParameters: ParameterInfo[];
115+
readonly name: string;
116+
readonly declarations: ConvertibleSignatureDeclaration[];
117+
}
118+
119+
interface ParameterInfo {
120+
readonly pos: number;
121+
readonly declaration: ParameterDeclaration;
122+
}
123+
124+
function getInfo(sourceFile: SourceFile, program: Program, pos: number): SignatureInfo | undefined {
125+
const token = getTokenAtPosition(sourceFile, pos);
126+
const callExpression = findAncestor(token, isCallExpression);
127+
if (callExpression === undefined || length(callExpression.arguments) === 0) {
128+
return undefined;
129+
}
130+
131+
const checker = program.getTypeChecker();
132+
const type = checker.getTypeAtLocation(callExpression.expression);
133+
const convertibleSignatureDeclarations = filter(type.symbol.declarations, isConvertibleSignatureDeclaration);
134+
if (convertibleSignatureDeclarations === undefined) {
135+
return undefined;
136+
}
137+
138+
const nonOverloadDeclaration = lastOrUndefined(convertibleSignatureDeclarations);
139+
if (
140+
nonOverloadDeclaration === undefined ||
141+
nonOverloadDeclaration.body === undefined ||
142+
isSourceFileFromLibrary(program, nonOverloadDeclaration.getSourceFile())
143+
) {
144+
return undefined;
145+
}
146+
147+
const name = tryGetName(nonOverloadDeclaration);
148+
if (name === undefined) {
149+
return undefined;
150+
}
151+
152+
const newParameters: ParameterInfo[] = [];
153+
const newOptionalParameters: ParameterInfo[] = [];
154+
const parametersLength = length(nonOverloadDeclaration.parameters);
155+
const argumentsLength = length(callExpression.arguments);
156+
if (parametersLength > argumentsLength) {
157+
return undefined;
158+
}
159+
160+
const declarations = [nonOverloadDeclaration, ...getOverloads(nonOverloadDeclaration, convertibleSignatureDeclarations)];
161+
for (let i = 0, pos = 0, paramIndex = 0; i < argumentsLength; i++) {
162+
const arg = callExpression.arguments[i];
163+
const expr = isAccessExpression(arg) ? getNameOfAccessExpression(arg) : arg;
164+
const type = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arg)));
165+
const parameter = pos < parametersLength ? nonOverloadDeclaration.parameters[pos] : undefined;
166+
if (
167+
parameter &&
168+
checker.isTypeAssignableTo(type, checker.getTypeAtLocation(parameter))
169+
) {
170+
pos++;
171+
continue;
172+
}
173+
174+
const name = expr && isIdentifier(expr) ? expr.text : `p${paramIndex++}`;
175+
const typeNode = typeToTypeNode(checker, type, nonOverloadDeclaration);
176+
append(newParameters, {
177+
pos: i,
178+
declaration: createParameter(name, typeNode, /*questionToken*/ undefined),
179+
});
180+
181+
if (isOptionalPos(declarations, pos)) {
182+
continue;
183+
}
184+
185+
append(newOptionalParameters, {
186+
pos: i,
187+
declaration: createParameter(name, typeNode, factory.createToken(SyntaxKind.QuestionToken)),
188+
});
189+
}
190+
191+
return {
192+
newParameters,
193+
newOptionalParameters,
194+
name: declarationNameToString(name),
195+
declarations,
196+
};
197+
}
198+
199+
function tryGetName(node: FunctionLikeDeclaration) {
200+
const name = getNameOfDeclaration(node);
201+
if (name) {
202+
return name;
203+
}
204+
205+
if (
206+
isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ||
207+
isPropertyDeclaration(node.parent) ||
208+
isParameter(node.parent)
209+
) {
210+
return node.parent.name;
211+
}
212+
}
213+
214+
function typeToTypeNode(checker: TypeChecker, type: Type, enclosingDeclaration: Node) {
215+
return checker.typeToTypeNode(checker.getWidenedType(type), enclosingDeclaration, NodeBuilderFlags.NoTruncation)
216+
?? factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword);
217+
}
218+
219+
function doChange(
220+
changes: textChanges.ChangeTracker,
221+
sourceFile: SourceFile,
222+
declarations: ConvertibleSignatureDeclaration[],
223+
newParameters: ParameterInfo[],
224+
) {
225+
forEach(declarations, declaration => {
226+
if (length(declaration.parameters)) {
227+
changes.replaceNodeRangeWithNodes(
228+
sourceFile,
229+
first(declaration.parameters),
230+
last(declaration.parameters),
231+
updateParameters(declaration, newParameters),
232+
{
233+
joiner: ", ",
234+
indentation: 0,
235+
leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll,
236+
trailingTriviaOption: textChanges.TrailingTriviaOption.Include,
237+
},
238+
);
239+
}
240+
else {
241+
forEach(updateParameters(declaration, newParameters), (parameter, index) => {
242+
if (length(declaration.parameters) === 0 && index === 0) {
243+
changes.insertNodeAt(sourceFile, declaration.parameters.end, parameter);
244+
}
245+
else {
246+
changes.insertNodeAtEndOfList(sourceFile, declaration.parameters, parameter);
247+
}
248+
});
249+
}
250+
});
251+
}
252+
253+
function isConvertibleSignatureDeclaration(node: Node): node is ConvertibleSignatureDeclaration {
254+
switch (node.kind) {
255+
case SyntaxKind.FunctionDeclaration:
256+
case SyntaxKind.FunctionExpression:
257+
case SyntaxKind.MethodDeclaration:
258+
case SyntaxKind.ArrowFunction:
259+
return true;
260+
default:
261+
return false;
262+
}
263+
}
264+
265+
function updateParameters(node: ConvertibleSignatureDeclaration, newParameters: readonly ParameterInfo[]) {
266+
const parameters = map(node.parameters, p =>
267+
factory.createParameterDeclaration(
268+
p.modifiers,
269+
p.dotDotDotToken,
270+
p.name,
271+
p.questionToken,
272+
p.type,
273+
p.initializer,
274+
));
275+
for (const { pos, declaration } of newParameters) {
276+
const prev = pos > 0 ? parameters[pos - 1] : undefined;
277+
parameters.splice(
278+
pos,
279+
0,
280+
factory.updateParameterDeclaration(
281+
declaration,
282+
declaration.modifiers,
283+
declaration.dotDotDotToken,
284+
declaration.name,
285+
prev && prev.questionToken ? factory.createToken(SyntaxKind.QuestionToken) : declaration.questionToken,
286+
declaration.type,
287+
declaration.initializer,
288+
),
289+
);
290+
}
291+
return parameters;
292+
}
293+
294+
function getOverloads(implementation: ConvertibleSignatureDeclaration, declarations: readonly ConvertibleSignatureDeclaration[]): ConvertibleSignatureDeclaration[] {
295+
const overloads: ConvertibleSignatureDeclaration[] = [];
296+
for (const declaration of declarations) {
297+
if (isOverload(declaration)) {
298+
if (length(declaration.parameters) === length(implementation.parameters)) {
299+
overloads.push(declaration);
300+
continue;
301+
}
302+
if (length(declaration.parameters) > length(implementation.parameters)) {
303+
return [];
304+
}
305+
}
306+
}
307+
return overloads;
308+
}
309+
310+
function isOverload(declaration: ConvertibleSignatureDeclaration) {
311+
return isConvertibleSignatureDeclaration(declaration) && declaration.body === undefined;
312+
}
313+
314+
function createParameter(name: string, type: TypeNode, questionToken: QuestionToken | undefined) {
315+
return factory.createParameterDeclaration(
316+
/*modifiers*/ undefined,
317+
/*dotDotDotToken*/ undefined,
318+
name,
319+
questionToken,
320+
type,
321+
/*initializer*/ undefined,
322+
);
323+
}
324+
325+
function isOptionalPos(declarations: ConvertibleSignatureDeclaration[], pos: number) {
326+
return length(declarations) && some(declarations, d => pos < length(d.parameters) && !!d.parameters[pos] && d.parameters[pos].questionToken === undefined);
327+
}

tests/cases/fourslash/arityErrorAfterSignatureHelp.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ verify.signatureHelp({
1818
kind: "retrigger"
1919
}
2020
})
21-
verify.not.codeFixAvailable() // trigger typecheck
21+
verify.not.codeFixAvailable(); // trigger typecheck
2222
verify.errorExistsBetweenMarkers("1", "2");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////[|function f() {}|]
4+
////
5+
////const a = 1;
6+
////f(a);
7+
8+
verify.codeFix({
9+
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "f"],
10+
index: 0,
11+
newRangeContent: "function f(a: number) {}"
12+
});

0 commit comments

Comments
 (0)