Skip to content

Commit

Permalink
feat(51870): add a quick fix to add an additional parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk committed Dec 20, 2023
1 parent 93e6b9d commit 9338f6a
Show file tree
Hide file tree
Showing 20 changed files with 503 additions and 2 deletions.
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -7784,6 +7784,18 @@
"category": "Message",
"code": 95187
},
"Add missing parameter to '{0}'": {
"category": "Message",
"code": 95188
},
"Add missing parameters to '{0}'": {
"category": "Message",
"code": 95189
},
"Add all missing parameters": {
"category": "Message",
"code": 95190
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
1 change: 1 addition & 0 deletions src/services/_namespaces/ts.codefix.ts
Expand Up @@ -31,6 +31,7 @@ export * from "../codefixes/fixSpelling";
export * from "../codefixes/returnValueCorrect";
export * from "../codefixes/fixAddMissingMember";
export * from "../codefixes/fixAddMissingNewOperator";
export * from "../codefixes/fixAddMissingParam";
export * from "../codefixes/fixCannotFindModule";
export * from "../codefixes/fixClassDoesntImplementInheritedAbstractMember";
export * from "../codefixes/fixClassSuperMustPrecedeThisAccess";
Expand Down
246 changes: 246 additions & 0 deletions src/services/codefixes/fixAddMissingParam.ts
@@ -0,0 +1,246 @@
import {
append,
ArrowFunction,
declarationNameToString,
DiagnosticOrDiagnosticAndArguments,
Diagnostics,
factory,
find,
findAncestor,
first,
forEach,
FunctionDeclaration,
FunctionExpression,
FunctionLikeDeclaration,
getNameOfAccessExpression,
getNameOfDeclaration,
getTokenAtPosition,
isAccessExpression,
isCallExpression,
isIdentifier,
isParameter,
isPropertyDeclaration,
isSourceFileFromLibrary,
isVariableDeclaration,
last,
lastOrUndefined,
length,
map,
MethodDeclaration,
Node,
NodeBuilderFlags,
ParameterDeclaration,
Program,
SignatureKind,
sort,
SourceFile,
SyntaxKind,
textChanges,
Type,
TypeChecker,
} from "../_namespaces/ts";
import {
codeFixAll,
createCodeFixAction,
registerCodeFix,
} from "../_namespaces/ts.codefix";

const fixId = "addMissingParam";
const errorCodes = [Diagnostics.Expected_0_arguments_but_got_1.code];

registerCodeFix({
errorCodes,
fixIds: [fixId],
getCodeActions(context) {
const info = getInfo(context.sourceFile, context.program, context.span.start);
if (info === undefined) return undefined;

const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, info));
return [createCodeFixAction(fixId, changes, info.description, fixId, Diagnostics.Add_all_missing_parameters)];
},
getAllCodeActions: context =>
codeFixAll(context, errorCodes, (changes, diag) => {
const info = getInfo(context.sourceFile, context.program, diag.start);
if (info) {
doChange(changes, context.sourceFile, info);
}
}),
});

type ConvertableSignatureDeclaration =
| FunctionDeclaration
| FunctionExpression
| ArrowFunction
| MethodDeclaration;

interface SignatureInfo {
readonly description: DiagnosticOrDiagnosticAndArguments;
readonly newParameters: ParameterInfo[];
readonly declarations: ConvertableSignatureDeclaration[];
readonly overload: ConvertableSignatureDeclaration | undefined;
}

interface ParameterInfo {
readonly pos: number;
readonly declaration: ParameterDeclaration;
}

function getInfo(sourceFile: SourceFile, program: Program, pos: number): SignatureInfo | undefined {
const token = getTokenAtPosition(sourceFile, pos);
const callExpression = findAncestor(token, isCallExpression);
if (callExpression === undefined || length(callExpression.arguments) === 0) return undefined;

const checker = program.getTypeChecker();
const type = checker.getTypeAtLocation(callExpression.expression);
const signatures = checker.getSignaturesOfType(type, SignatureKind.Call);
const signature = lastOrUndefined(sort(signatures, (a, b) => length(a.parameters) - length(b.parameters)));
if (
signature &&
signature.declaration &&
isConvertableSignatureDeclaration(signature.declaration)
) {
const declaration = (signature.declaration.body === undefined ? find(signature.declaration.symbol.declarations, d => isConvertableSignatureDeclaration(d) && !!d.body) : signature.declaration) as ConvertableSignatureDeclaration;
if (declaration === undefined) {
return undefined;
}

isSourceFileFromLibrary(program, declaration.getSourceFile());

const overload = signature.declaration.body === undefined ? signature.declaration : undefined;
if (overload && length(overload.parameters) !== length(declaration.parameters)) return undefined;

const name = tryGetName(declaration);
if (name === undefined) return undefined;

const declarations: ConvertableSignatureDeclaration[] = append([declaration], overload);
const newParameters: ParameterInfo[] = [];
const parametersLength = length(declaration.parameters);
const argumentsLength = length(callExpression.arguments);
for (let i = 0, pos = 0, paramIndex = 0; i < argumentsLength; i++) {
const arg = callExpression.arguments[i];
const expr = isAccessExpression(arg) ? getNameOfAccessExpression(arg) : arg;
const type = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arg)));
const parameter = pos < parametersLength ? declaration.parameters[pos] : undefined;
if (
parameter &&
checker.isTypeAssignableTo(type, checker.getTypeAtLocation(parameter))
) {
pos++;
}
else {
const newParameter = {
pos: i,
declaration: factory.createParameterDeclaration(
/*modifiers*/ undefined,
/*dotDotDotToken*/ undefined,
expr && isIdentifier(expr) ? expr.text : `p${paramIndex++}`,
/*questionToken*/ undefined,
typeToTypeNode(checker, type, declaration),
/*initializer*/ undefined,
),
};
append(newParameters, newParameter);
}
}
return {
declarations,
newParameters,
overload,
description: [
length(newParameters) > 1 ? Diagnostics.Add_missing_parameters_to_0 : Diagnostics.Add_missing_parameter_to_0,
declarationNameToString(name),
],
};
}
return undefined;
}

function tryGetName(node: FunctionLikeDeclaration) {
const name = getNameOfDeclaration(node);
if (name) {
return name;
}

if (
isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ||
isPropertyDeclaration(node.parent) ||
isParameter(node.parent)
) {
return node.parent.name;
}
}

function typeToTypeNode(checker: TypeChecker, type: Type, enclosingDeclaration: Node) {
const typeNode = checker.typeToTypeNode(checker.getWidenedType(type), enclosingDeclaration, NodeBuilderFlags.NoTruncation);
return typeNode ? typeNode : factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword);
}

function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, { declarations, newParameters }: SignatureInfo) {
forEach(declarations, declaration => {
if (length(declaration.parameters)) {
changes.replaceNodeRangeWithNodes(
sourceFile,
first(declaration.parameters),
last(declaration.parameters),
updateParameters(declaration, newParameters),
{
joiner: ", ",
indentation: 0,
leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll,
trailingTriviaOption: textChanges.TrailingTriviaOption.Include,
},
);
}
else {
forEach(updateParameters(declaration, newParameters), (parameter, index) => {
if (length(declaration.parameters) === 0 && index === 0) {
changes.insertNodeAt(sourceFile, declaration.parameters.end, parameter);
}
else {
changes.insertNodeAtEndOfList(sourceFile, declaration.parameters, parameter);
}
});
}
});
}

function isConvertableSignatureDeclaration(node: Node): node is ConvertableSignatureDeclaration {
switch (node.kind) {
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.ArrowFunction:
return true;
default:
return false;
}
}

function updateParameters(node: ConvertableSignatureDeclaration, newParameters: readonly ParameterInfo[]) {
const parameters = map(node.parameters, p =>
factory.createParameterDeclaration(
p.modifiers,
p.dotDotDotToken,
p.name,
p.questionToken,
p.type,
p.initializer,
));
for (const { pos, declaration } of newParameters) {
const prev = pos > 0 ? parameters[pos - 1] : undefined;
parameters.splice(
pos,
0,
factory.updateParameterDeclaration(
declaration,
declaration.modifiers,
declaration.dotDotDotToken,
declaration.name,
prev && prev.questionToken ? factory.createToken(SyntaxKind.QuestionToken) : declaration.questionToken,
declaration.type,
declaration.initializer,
),
);
}
return parameters;
}
2 changes: 1 addition & 1 deletion tests/cases/fourslash/arityErrorAfterSignatureHelp.ts
Expand Up @@ -18,5 +18,5 @@ verify.signatureHelp({
kind: "retrigger"
}
})
verify.not.codeFixAvailable() // trigger typecheck
verify.not.codeFixAvailable(); // trigger typecheck
verify.errorExistsBetweenMarkers("1", "2");
12 changes: 12 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam1.ts
@@ -0,0 +1,12 @@
/// <reference path="fourslash.ts" />

////[|function f() {}|]
////
////const a = 1;
////f(a);

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "f"],
index: 0,
newRangeContent: "function f(a: number) {}"
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam10.ts
@@ -0,0 +1,13 @@
/// <reference path="fourslash.ts" />

////[|function f() {}|]
////
////const a = 1;
////const b = "";
////f(a, b, true);

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameters_to_0.message, "f"],
index: 0,
newRangeContent: "function f(a: number, b: string, p0: boolean) {}"
});
14 changes: 14 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam11.ts
@@ -0,0 +1,14 @@
/// <reference path="fourslash.ts" />

////class C {
//// [|private p = () => {}|]
//// m(a: boolean) {
//// this.p(a);
//// }
////}

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "p"],
index: 0,
newRangeContent: "private p = (a: boolean) => {}"
});
11 changes: 11 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam12.ts
@@ -0,0 +1,11 @@
/// <reference path="fourslash.ts" />

////function f([|cb = () => {}|]) {
//// cb("");
////}

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "cb"],
index: 0,
newRangeContent: "cb = (p0: string) => {}"
});
20 changes: 20 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam13.ts
@@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />

////function f(a: string): string;
////function f(a: string, b: number): string;
////function f(a: string, b?: number): string {
//// return "";
////}
////f("", 1, "");

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "f"],
index: 0,
newFileContent:
`function f(a: string): string;
function f(a: string, b: number, p0: string): string;
function f(a: string, b?: number, p0?: string): string {
return "";
}
f("", 1, "");`
});
20 changes: 20 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam14.ts
@@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />

////function f(a: string): string;
////function f(a: string, b: number): string;
////function f(a: string, b?: number): string {
//// return "";
////}
////f("", "", 1);

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "f"],
index: 0,
newFileContent:
`function f(a: string): string;
function f(a: string, p0: string, b: number): string;
function f(a: string, p0: string, b?: number): string {
return "";
}
f("", "", 1);`
});
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingParam2.ts
@@ -0,0 +1,13 @@
/// <reference path="fourslash.ts" />

////[|function f(a: number) {}|]
////
////const a = 1;
////const b = 1;
////f(a, b);

verify.codeFix({
description: [ts.Diagnostics.Add_missing_parameter_to_0.message, "f"],
index: 0,
newRangeContent: "function f(a: number, b: number) {}"
});

0 comments on commit 9338f6a

Please sign in to comment.