diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 6cd6d0d7f18b3..f08e86983a8af 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -15,6 +15,15 @@ namespace ts.codefix { Diagnostics.Cannot_find_name_0.code ]; + enum InfoKind { + TypeLikeDeclaration, + Enum, + Function, + ObjectLiteral, + JsxAttributes, + Signature, + } + registerCodeFix({ errorCodes, getCodeActions(context) { @@ -31,7 +40,7 @@ namespace ts.codefix { const changes = textChanges.ChangeTracker.with(context, t => addJsxAttributes(t, context, info)); return [createCodeFixAction(fixMissingAttributes, changes, Diagnostics.Add_missing_attributes, fixMissingAttributes, Diagnostics.Add_all_missing_attributes)]; } - if (info.kind === InfoKind.Function) { + if (info.kind === InfoKind.Function || info.kind === InfoKind.Signature) { const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info)); return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)]; } @@ -54,8 +63,7 @@ namespace ts.codefix { if (!info || !addToSeen(seen, getNodeId(info.parentDeclaration) + "#" + info.token.text)) { return; } - - if (fixId === fixMissingFunctionDeclaration && info.kind === InfoKind.Function) { + if (fixId === fixMissingFunctionDeclaration && (info.kind === InfoKind.Function || info.kind === InfoKind.Signature)) { addFunctionDeclaration(changes, context, info); } else if (fixId === fixMissingProperties && info.kind === InfoKind.ObjectLiteral) { @@ -107,8 +115,7 @@ namespace ts.codefix { }, }); - const enum InfoKind { TypeLikeDeclaration, Enum, Function, ObjectLiteral, JsxAttributes } - type Info = TypeLikeDeclarationInfo | EnumInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo; + type Info = TypeLikeDeclarationInfo | EnumInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo | SignatureInfo; interface EnumInfo { readonly kind: InfoKind.Enum; @@ -132,7 +139,7 @@ namespace ts.codefix { readonly token: Identifier; readonly sourceFile: SourceFile; readonly modifierFlags: ModifierFlags; - readonly parentDeclaration: SourceFile | ModuleDeclaration; + readonly parentDeclaration: SourceFile | ModuleDeclaration | ReturnStatement; } interface ObjectLiteralInfo { @@ -150,6 +157,14 @@ namespace ts.codefix { readonly parentDeclaration: JsxOpeningLikeElement; } + interface SignatureInfo { + readonly kind: InfoKind.Signature; + readonly token: Identifier; + readonly signature: Signature; + readonly sourceFile: SourceFile; + readonly parentDeclaration: Node; + } + function getInfo(sourceFile: SourceFile, tokenPos: number, errorCode: number, checker: TypeChecker, program: Program): Info | undefined { // The identifier of the missing property. eg: // this.missing = 1; @@ -190,8 +205,16 @@ namespace ts.codefix { return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent }; } - if (isIdentifier(token) && isCallExpression(parent) && parent.expression === token) { - return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile }; + if (isIdentifier(token)) { + const type = checker.getContextualType(token); + if (type && getObjectFlags(type) & ObjectFlags.Anonymous) { + const signature = firstOrUndefined(checker.getSignaturesOfType(type, SignatureKind.Call)); + if (signature === undefined) return undefined; + return { kind: InfoKind.Signature, token, signature, sourceFile, parentDeclaration: findScope(token) }; + } + if (isCallExpression(parent) && parent.expression === token) { + return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: findScope(token) }; + } } if (!isPropertyAccessExpression(parent)) return undefined; @@ -451,10 +474,19 @@ namespace ts.codefix { }); } - function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo) { + function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo | SignatureInfo) { + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const functionDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) as FunctionDeclaration; - changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration); + const functionDeclaration = info.kind === InfoKind.Function + ? createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) + : createSignatureDeclarationFromSignature(SyntaxKind.FunctionDeclaration, context, quotePreference, info.signature, createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), info.token, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ undefined, importAdder); + if (functionDeclaration === undefined) { + Debug.fail("fixMissingFunctionDeclaration codefix got unexpected error."); + } + + isReturnStatement(info.parentDeclaration) + ? changes.insertNodeBefore(info.sourceFile, info.parentDeclaration, functionDeclaration, /*blankLineBetween*/ true) + : changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration); importAdder.writeFixes(changes); } @@ -618,4 +650,12 @@ namespace ts.codefix { } return createPropertyNameNodeForIdentifierOrLiteral(symbol.name, target, quotePreference === QuotePreference.Single); } + + function findScope(node: Node) { + if (findAncestor(node, isJsxExpression)) { + const returnStatement = findAncestor(node.parent, isReturnStatement); + if (returnStatement) return returnStatement; + } + return getSourceFileOfNode(node); + } } diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 7300ddf868ab9..7562e2320c2b7 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -169,13 +169,17 @@ namespace ts.codefix { } function outputMethod(quotePreference: QuotePreference, signature: Signature, modifiers: NodeArray | undefined, name: PropertyName, body?: Block): void { - const method = createSignatureDeclarationFromSignature(SyntaxKind.MethodDeclaration, context, quotePreference, signature, body, name, modifiers, optional && !!(preserveOptional & PreserveOptionalFlags.Method), enclosingDeclaration, importAdder); + const method = createSignatureDeclarationFromSignature(SyntaxKind.MethodDeclaration, context, quotePreference, signature, body, name, modifiers, optional && !!(preserveOptional & PreserveOptionalFlags.Method), enclosingDeclaration, importAdder) as MethodDeclaration; if (method) addClassElement(method); } } export function createSignatureDeclarationFromSignature( - kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionExpression | SyntaxKind.ArrowFunction, + kind: + | SyntaxKind.MethodDeclaration + | SyntaxKind.FunctionExpression + | SyntaxKind.ArrowFunction + | SyntaxKind.FunctionDeclaration, context: TypeConstructionContext, quotePreference: QuotePreference, signature: Signature, @@ -185,7 +189,7 @@ namespace ts.codefix { optional: boolean | undefined, enclosingDeclaration: Node | undefined, importAdder: ImportAdder | undefined - ) { + ) { const program = context.program; const checker = program.getTypeChecker(); const scriptTarget = getEmitScriptTarget(program.getCompilerOptions()); @@ -194,7 +198,7 @@ namespace ts.codefix { | NodeBuilderFlags.SuppressAnyReturnType | NodeBuilderFlags.AllowEmptyTuple | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); - const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as ArrowFunction | FunctionExpression | MethodDeclaration; + const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as ArrowFunction | FunctionExpression | MethodDeclaration | FunctionDeclaration; if (!signatureDeclaration) { return undefined; } @@ -273,6 +277,9 @@ namespace ts.codefix { if (isMethodDeclaration(signatureDeclaration)) { return factory.updateMethodDeclaration(signatureDeclaration, modifiers, asteriskToken, name ?? factory.createIdentifier(""), questionToken, typeParameters, parameters, type, body); } + if (isFunctionDeclaration(signatureDeclaration)) { + return factory.updateFunctionDeclaration(signatureDeclaration, modifiers, signatureDeclaration.asteriskToken, tryCast(name, isIdentifier), typeParameters, parameters, type, body ?? signatureDeclaration.body); + } return undefined; } diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration21.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration21.ts new file mode 100644 index 0000000000000..a5689161936bb --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration21.ts @@ -0,0 +1,39 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// onClick: (a: number, b: string) => void; +////} +//// +////const A = ({ onClick }: P) => +////
; +//// +////const B = () => { +//// return ( +//// +//// ); +////} + +verify.codeFix({ + index: 0, + description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "handleClick"], + newFileContent: +`interface P { + onClick: (a: number, b: string) => void; +} + +const A = ({ onClick }: P) => +
; + +const B = () => { + function handleClick(a: number, b: string): void { + throw new Error("Function not implemented."); + } + + return ( + + ); +}` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration22.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration22.ts new file mode 100644 index 0000000000000..a20260a0072c0 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration22.ts @@ -0,0 +1,34 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////interface P { +//// onClick: (a: number, b: string) => void; +////} +//// +////const A = ({ onClick }: P) => +////
; +//// +////const B = () => +//// + +verify.codeFix({ + index: 0, + description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "handleClick"], + newFileContent: +`interface P { + onClick: (a: number, b: string) => void; +} + +const A = ({ onClick }: P) => +
; + +const B = () => + + +function handleClick(a: number, b: string): void { + throw new Error("Function not implemented."); +} +` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration23.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration23.ts new file mode 100644 index 0000000000000..612e226f4d8dc --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration23.ts @@ -0,0 +1,21 @@ +/// + +// @jsx: preserve +// @filename: foo.tsx + +////const A = () => { +//// return (
handleClick()}>
); +////} + +verify.codeFix({ + index: 0, + description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "handleClick"], + newFileContent: +`const A = () => { + function handleClick() { + throw new Error("Function not implemented."); + } + + return (
handleClick()}>
); +}` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration24.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration24.ts new file mode 100644 index 0000000000000..f14ed8cb950dc --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration24.ts @@ -0,0 +1,27 @@ +/// + +////interface Foo { +//// a: (e: any) => void; +////} +//// +////const foo: Foo = { +//// a: fn +////} + +verify.codeFix({ + index: 0, + description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "fn"], + newFileContent: +`interface Foo { + a: (e: any) => void; +} + +const foo: Foo = { + a: fn +} + +function fn(e: any): void { + throw new Error("Function not implemented."); +} +` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration25.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration25.ts new file mode 100644 index 0000000000000..c575e80159396 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration25.ts @@ -0,0 +1,23 @@ +/// + +////interface Foo { +//// f(type: string, listener: (this: object, type: string) => any): void; +////} +////declare let foo: Foo; +////foo.f("test", fn); + +verify.codeFix({ + index: 0, + description: [ts.Diagnostics.Add_missing_function_declaration_0.message, "fn"], + newFileContent: +`interface Foo { + f(type: string, listener: (this: object, type: string) => any): void; +} +declare let foo: Foo; +foo.f("test", fn); + +function fn(this: object, type: string) { + throw new Error("Function not implemented."); +} +` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_all.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_all.ts index 7f7e338dab577..1736613e98f7c 100644 --- a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_all.ts +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_all.ts @@ -1,33 +1,38 @@ /// -// @filename: /test.ts +// @filename: /test1.ts ////export const x = 1; -// @filename: /foo.ts -////import * as test from "./test"; +// @filename: /test2.ts +////import * as test from "./test1"; //// ////namespace Foo { //// export const x = 0; ////} //// +////interface I { +//// a: (e: any) => void; +////} +//// ////test.f(); ////Foo.f(); ////f(); +////const t1: I = { a: fn } -goTo.file("/foo.ts"); +goTo.file("/test2.ts"); verify.codeFixAll({ fixId: "fixMissingFunctionDeclaration", fixAllDescription: ts.Diagnostics.Add_all_missing_function_declarations.message, newFileContent: { - "/test.ts": + "/test1.ts": `export const x = 1; export function f() { throw new Error("Function not implemented."); } `, - "/foo.ts": -`import * as test from "./test"; + "/test2.ts": +`import * as test from "./test1"; namespace Foo { export const x = 0; @@ -37,13 +42,23 @@ namespace Foo { } } +interface I { + a: (e: any) => void; +} + test.f(); Foo.f(); f(); +const t1: I = { a: fn } function f() { throw new Error("Function not implemented."); } + + +function fn(e: any): void { + throw new Error("Function not implemented."); +} ` } }); diff --git a/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_jsx_all.ts b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_jsx_all.ts new file mode 100644 index 0000000000000..3bbf54c66fb0b --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingFunctionDeclaration_jsx_all.ts @@ -0,0 +1,53 @@ +/// + +// @jsx: preserve +// @filename: /foo.tsx + +////interface P { +//// onClick: (a: number, b: string) => void; +////} +//// +////const A = ({ onClick }: P) => +////
; +//// +////const B = () => +//// +//// +////const C = () => { +//// return ( +//// +//// ); +////} + +goTo.file("/foo.tsx"); +verify.codeFixAll({ + fixId: "fixMissingFunctionDeclaration", + fixAllDescription: ts.Diagnostics.Add_all_missing_function_declarations.message, + newFileContent: { + "/foo.tsx": +`interface P { + onClick: (a: number, b: string) => void; +} + +const A = ({ onClick }: P) => +
; + +const B = () => + + +const C = () => { + function handleClick(a: number, b: string): void { + throw new Error("Function not implemented."); + } + + return ( + + ); +} + +function handleClick(a: number, b: string): void { + throw new Error("Function not implemented."); +} +` + } +});