Skip to content

Commit f70cb76

Browse files
authored
feat(49928): Provide quick fix for a missing callback function (microsoft#49930)
* feat(49928): provide quick fix for a missing callback function * remove addFunctionDeclarationFromSignature. fix formatting * add tests
1 parent a123fc5 commit f70cb76

9 files changed

+281
-22
lines changed

src/services/codefixes/fixAddMissingMember.ts

+51-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ namespace ts.codefix {
1515
Diagnostics.Cannot_find_name_0.code
1616
];
1717

18+
enum InfoKind {
19+
TypeLikeDeclaration,
20+
Enum,
21+
Function,
22+
ObjectLiteral,
23+
JsxAttributes,
24+
Signature,
25+
}
26+
1827
registerCodeFix({
1928
errorCodes,
2029
getCodeActions(context) {
@@ -31,7 +40,7 @@ namespace ts.codefix {
3140
const changes = textChanges.ChangeTracker.with(context, t => addJsxAttributes(t, context, info));
3241
return [createCodeFixAction(fixMissingAttributes, changes, Diagnostics.Add_missing_attributes, fixMissingAttributes, Diagnostics.Add_all_missing_attributes)];
3342
}
34-
if (info.kind === InfoKind.Function) {
43+
if (info.kind === InfoKind.Function || info.kind === InfoKind.Signature) {
3544
const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info));
3645
return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)];
3746
}
@@ -54,8 +63,7 @@ namespace ts.codefix {
5463
if (!info || !addToSeen(seen, getNodeId(info.parentDeclaration) + "#" + info.token.text)) {
5564
return;
5665
}
57-
58-
if (fixId === fixMissingFunctionDeclaration && info.kind === InfoKind.Function) {
66+
if (fixId === fixMissingFunctionDeclaration && (info.kind === InfoKind.Function || info.kind === InfoKind.Signature)) {
5967
addFunctionDeclaration(changes, context, info);
6068
}
6169
else if (fixId === fixMissingProperties && info.kind === InfoKind.ObjectLiteral) {
@@ -107,8 +115,7 @@ namespace ts.codefix {
107115
},
108116
});
109117

110-
const enum InfoKind { TypeLikeDeclaration, Enum, Function, ObjectLiteral, JsxAttributes }
111-
type Info = TypeLikeDeclarationInfo | EnumInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo;
118+
type Info = TypeLikeDeclarationInfo | EnumInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo | SignatureInfo;
112119

113120
interface EnumInfo {
114121
readonly kind: InfoKind.Enum;
@@ -132,7 +139,7 @@ namespace ts.codefix {
132139
readonly token: Identifier;
133140
readonly sourceFile: SourceFile;
134141
readonly modifierFlags: ModifierFlags;
135-
readonly parentDeclaration: SourceFile | ModuleDeclaration;
142+
readonly parentDeclaration: SourceFile | ModuleDeclaration | ReturnStatement;
136143
}
137144

138145
interface ObjectLiteralInfo {
@@ -150,6 +157,14 @@ namespace ts.codefix {
150157
readonly parentDeclaration: JsxOpeningLikeElement;
151158
}
152159

160+
interface SignatureInfo {
161+
readonly kind: InfoKind.Signature;
162+
readonly token: Identifier;
163+
readonly signature: Signature;
164+
readonly sourceFile: SourceFile;
165+
readonly parentDeclaration: Node;
166+
}
167+
153168
function getInfo(sourceFile: SourceFile, tokenPos: number, errorCode: number, checker: TypeChecker, program: Program): Info | undefined {
154169
// The identifier of the missing property. eg:
155170
// this.missing = 1;
@@ -190,8 +205,16 @@ namespace ts.codefix {
190205
return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent };
191206
}
192207

193-
if (isIdentifier(token) && isCallExpression(parent) && parent.expression === token) {
194-
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
208+
if (isIdentifier(token)) {
209+
const type = checker.getContextualType(token);
210+
if (type && getObjectFlags(type) & ObjectFlags.Anonymous) {
211+
const signature = firstOrUndefined(checker.getSignaturesOfType(type, SignatureKind.Call));
212+
if (signature === undefined) return undefined;
213+
return { kind: InfoKind.Signature, token, signature, sourceFile, parentDeclaration: findScope(token) };
214+
}
215+
if (isCallExpression(parent) && parent.expression === token) {
216+
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: findScope(token) };
217+
}
195218
}
196219

197220
if (!isPropertyAccessExpression(parent)) return undefined;
@@ -451,10 +474,19 @@ namespace ts.codefix {
451474
});
452475
}
453476

454-
function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo) {
477+
function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo | SignatureInfo) {
478+
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
455479
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
456-
const functionDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) as FunctionDeclaration;
457-
changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration);
480+
const functionDeclaration = info.kind === InfoKind.Function
481+
? createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration)
482+
: createSignatureDeclarationFromSignature(SyntaxKind.FunctionDeclaration, context, quotePreference, info.signature, createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), info.token, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ undefined, importAdder);
483+
if (functionDeclaration === undefined) {
484+
Debug.fail("fixMissingFunctionDeclaration codefix got unexpected error.");
485+
}
486+
487+
isReturnStatement(info.parentDeclaration)
488+
? changes.insertNodeBefore(info.sourceFile, info.parentDeclaration, functionDeclaration, /*blankLineBetween*/ true)
489+
: changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration);
458490
importAdder.writeFixes(changes);
459491
}
460492

@@ -618,4 +650,12 @@ namespace ts.codefix {
618650
}
619651
return createPropertyNameNodeForIdentifierOrLiteral(symbol.name, target, quotePreference === QuotePreference.Single);
620652
}
653+
654+
function findScope(node: Node) {
655+
if (findAncestor(node, isJsxExpression)) {
656+
const returnStatement = findAncestor(node.parent, isReturnStatement);
657+
if (returnStatement) return returnStatement;
658+
}
659+
return getSourceFileOfNode(node);
660+
}
621661
}

src/services/codefixes/helpers.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,17 @@ namespace ts.codefix {
169169
}
170170

171171
function outputMethod(quotePreference: QuotePreference, signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
172-
const method = createSignatureDeclarationFromSignature(SyntaxKind.MethodDeclaration, context, quotePreference, signature, body, name, modifiers, optional && !!(preserveOptional & PreserveOptionalFlags.Method), enclosingDeclaration, importAdder);
172+
const method = createSignatureDeclarationFromSignature(SyntaxKind.MethodDeclaration, context, quotePreference, signature, body, name, modifiers, optional && !!(preserveOptional & PreserveOptionalFlags.Method), enclosingDeclaration, importAdder) as MethodDeclaration;
173173
if (method) addClassElement(method);
174174
}
175175
}
176176

177177
export function createSignatureDeclarationFromSignature(
178-
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionExpression | SyntaxKind.ArrowFunction,
178+
kind:
179+
| SyntaxKind.MethodDeclaration
180+
| SyntaxKind.FunctionExpression
181+
| SyntaxKind.ArrowFunction
182+
| SyntaxKind.FunctionDeclaration,
179183
context: TypeConstructionContext,
180184
quotePreference: QuotePreference,
181185
signature: Signature,
@@ -185,7 +189,7 @@ namespace ts.codefix {
185189
optional: boolean | undefined,
186190
enclosingDeclaration: Node | undefined,
187191
importAdder: ImportAdder | undefined
188-
) {
192+
) {
189193
const program = context.program;
190194
const checker = program.getTypeChecker();
191195
const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
@@ -194,7 +198,7 @@ namespace ts.codefix {
194198
| NodeBuilderFlags.SuppressAnyReturnType
195199
| NodeBuilderFlags.AllowEmptyTuple
196200
| (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None);
197-
const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as ArrowFunction | FunctionExpression | MethodDeclaration;
201+
const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as ArrowFunction | FunctionExpression | MethodDeclaration | FunctionDeclaration;
198202
if (!signatureDeclaration) {
199203
return undefined;
200204
}
@@ -273,6 +277,9 @@ namespace ts.codefix {
273277
if (isMethodDeclaration(signatureDeclaration)) {
274278
return factory.updateMethodDeclaration(signatureDeclaration, modifiers, asteriskToken, name ?? factory.createIdentifier(""), questionToken, typeParameters, parameters, type, body);
275279
}
280+
if (isFunctionDeclaration(signatureDeclaration)) {
281+
return factory.updateFunctionDeclaration(signatureDeclaration, modifiers, signatureDeclaration.asteriskToken, tryCast(name, isIdentifier), typeParameters, parameters, type, body ?? signatureDeclaration.body);
282+
}
276283
return undefined;
277284
}
278285

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// onClick: (a: number, b: string) => void;
8+
////}
9+
////
10+
////const A = ({ onClick }: P) =>
11+
//// <div onClick={onClick}></div>;
12+
////
13+
////const B = () => {
14+
//// return (
15+
//// <A onClick={handleClick}></A>
16+
//// );
17+
////}
18+
19+
verify.codeFix({
20+
index: 0,
21+
description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "handleClick"],
22+
newFileContent:
23+
`interface P {
24+
onClick: (a: number, b: string) => void;
25+
}
26+
27+
const A = ({ onClick }: P) =>
28+
<div onClick={onClick}></div>;
29+
30+
const B = () => {
31+
function handleClick(a: number, b: string): void {
32+
throw new Error("Function not implemented.");
33+
}
34+
35+
return (
36+
<A onClick={handleClick}></A>
37+
);
38+
}`
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////interface P {
7+
//// onClick: (a: number, b: string) => void;
8+
////}
9+
////
10+
////const A = ({ onClick }: P) =>
11+
//// <div onClick={onClick}></div>;
12+
////
13+
////const B = () =>
14+
//// <A onClick={handleClick}></A>
15+
16+
verify.codeFix({
17+
index: 0,
18+
description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "handleClick"],
19+
newFileContent:
20+
`interface P {
21+
onClick: (a: number, b: string) => void;
22+
}
23+
24+
const A = ({ onClick }: P) =>
25+
<div onClick={onClick}></div>;
26+
27+
const B = () =>
28+
<A onClick={handleClick}></A>
29+
30+
function handleClick(a: number, b: string): void {
31+
throw new Error("Function not implemented.");
32+
}
33+
`
34+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: preserve
4+
// @filename: foo.tsx
5+
6+
////const A = () => {
7+
//// return (<div onClick={() => handleClick()}></div>);
8+
////}
9+
10+
verify.codeFix({
11+
index: 0,
12+
description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "handleClick"],
13+
newFileContent:
14+
`const A = () => {
15+
function handleClick() {
16+
throw new Error("Function not implemented.");
17+
}
18+
19+
return (<div onClick={() => handleClick()}></div>);
20+
}`
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// a: (e: any) => void;
5+
////}
6+
////
7+
////const foo: Foo = {
8+
//// a: fn
9+
////}
10+
11+
verify.codeFix({
12+
index: 0,
13+
description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "fn"],
14+
newFileContent:
15+
`interface Foo {
16+
a: (e: any) => void;
17+
}
18+
19+
const foo: Foo = {
20+
a: fn
21+
}
22+
23+
function fn(e: any): void {
24+
throw new Error("Function not implemented.");
25+
}
26+
`
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// f(type: string, listener: (this: object, type: string) => any): void;
5+
////}
6+
////declare let foo: Foo;
7+
////foo.f("test", fn);
8+
9+
verify.codeFix({
10+
index: 0,
11+
description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "fn"],
12+
newFileContent:
13+
`interface Foo {
14+
f(type: string, listener: (this: object, type: string) => any): void;
15+
}
16+
declare let foo: Foo;
17+
foo.f("test", fn);
18+
19+
function fn(this: object, type: string) {
20+
throw new Error("Function not implemented.");
21+
}
22+
`
23+
});
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
/// <reference path='fourslash.ts' />
22

3-
// @filename: /test.ts
3+
// @filename: /test1.ts
44
////export const x = 1;
55

6-
// @filename: /foo.ts
7-
////import * as test from "./test";
6+
// @filename: /test2.ts
7+
////import * as test from "./test1";
88
////
99
////namespace Foo {
1010
//// export const x = 0;
1111
////}
1212
////
13+
////interface I {
14+
//// a: (e: any) => void;
15+
////}
16+
////
1317
////test.f();
1418
////Foo.f();
1519
////f();
20+
////const t1: I = { a: fn }
1621

17-
goTo.file("/foo.ts");
22+
goTo.file("/test2.ts");
1823
verify.codeFixAll({
1924
fixId: "fixMissingFunctionDeclaration",
2025
fixAllDescription: ts.Diagnostics.Add_all_missing_function_declarations.message,
2126
newFileContent: {
22-
"/test.ts":
27+
"/test1.ts":
2328
`export const x = 1;
2429
2530
export function f() {
2631
throw new Error("Function not implemented.");
2732
}
2833
`,
29-
"/foo.ts":
30-
`import * as test from "./test";
34+
"/test2.ts":
35+
`import * as test from "./test1";
3136
3237
namespace Foo {
3338
export const x = 0;
@@ -37,13 +42,23 @@ namespace Foo {
3742
}
3843
}
3944
45+
interface I {
46+
a: (e: any) => void;
47+
}
48+
4049
test.f();
4150
Foo.f();
4251
f();
52+
const t1: I = { a: fn }
4353
4454
function f() {
4555
throw new Error("Function not implemented.");
4656
}
57+
58+
59+
function fn(e: any): void {
60+
throw new Error("Function not implemented.");
61+
}
4762
`
4863
}
4964
});

0 commit comments

Comments
 (0)