diff --git a/README.md b/README.md index 373a0d4..bf86801 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ situations where a nested object has missing members: ![Missing constructor members](gifs/missing-constructor-argument-members.gif) +#### Missing function/method return members + +![Missing function/method return members](gifs/missing-method-return-members.gif) + ## Requirements N/A diff --git a/gifs/missing-method-return-members.gif b/gifs/missing-method-return-members.gif new file mode 100644 index 0000000..61364e8 Binary files /dev/null and b/gifs/missing-method-return-members.gif differ diff --git a/packages/e2e/src/tests/extension.spec.ts b/packages/e2e/src/tests/extension.spec.ts index fb768d9..79023e3 100644 --- a/packages/e2e/src/tests/extension.spec.ts +++ b/packages/e2e/src/tests/extension.spec.ts @@ -90,14 +90,16 @@ describe('Acceptance tests', () => { const testingDocument = await vscode.workspace.openTextDocument(testFileUri) await vscode.window.showTextDocument(testingDocument) + const methodReturnAction = await getCodeAction(testingDocument, `return {}`, ACTION_NAME) + await vscode.workspace.applyEdit(methodReturnAction.edit!) + expect(getAllDocumentText(testingDocument)).toContain(`{\n status: 'todo'\n}`) + const constructorAction = await getCodeAction(testingDocument, `{ timeout: 456 }`, ACTION_NAME) await vscode.workspace.applyEdit(constructorAction.edit!) - const methodAction = await getCodeAction(testingDocument, `makeRequest({})`, ACTION_NAME) - await vscode.workspace.applyEdit(methodAction.edit!) - - const documentText = getAllDocumentText(testingDocument) - expect(documentText).toContain(await readFixture('new-http-client')) + const methodCallAction = await getCodeAction(testingDocument, `makeRequest({})`, ACTION_NAME) + await vscode.workspace.applyEdit(methodCallAction.edit!) + expect(getAllDocumentText(testingDocument)).toContain(await readFixture('new-http-client')) }) }) }) diff --git a/packages/extension/README.md b/packages/extension/README.md index 373a0d4..bf86801 100644 --- a/packages/extension/README.md +++ b/packages/extension/README.md @@ -19,6 +19,10 @@ situations where a nested object has missing members: ![Missing constructor members](gifs/missing-constructor-argument-members.gif) +#### Missing function/method return members + +![Missing function/method return members](gifs/missing-method-return-members.gif) + ## Requirements N/A diff --git a/packages/extension/gifs/missing-method-return-members.gif b/packages/extension/gifs/missing-method-return-members.gif new file mode 100644 index 0000000..61364e8 Binary files /dev/null and b/packages/extension/gifs/missing-method-return-members.gif differ diff --git a/packages/plugin/README.md b/packages/plugin/README.md index 1458b35..9ba34be 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -19,6 +19,10 @@ situations where a nested object has missing members: ![Missing constructor members](gifs/missing-constructor-argument-members.gif) +#### Missing function/method return members + +![Missing function/method return members](gifs/missing-method-return-members.gif) + ## Requirements N/A diff --git a/packages/plugin/gifs/missing-method-return-members.gif b/packages/plugin/gifs/missing-method-return-members.gif new file mode 100644 index 0000000..61364e8 Binary files /dev/null and b/packages/plugin/gifs/missing-method-return-members.gif differ diff --git a/packages/plugin/src/code-fixes/declare-missing-object-members.spec.ts b/packages/plugin/src/code-fixes/declare-missing-object-members.spec.ts index 9a2df61..ef79b7e 100644 --- a/packages/plugin/src/code-fixes/declare-missing-object-members.spec.ts +++ b/packages/plugin/src/code-fixes/declare-missing-object-members.spec.ts @@ -292,53 +292,97 @@ describe('declareMissingObjectMembers', () => { expect(newText).toMatchInitializer({ greeting: 'todo', name: 'todo' }) }) - it('works for class constructors', () => { - const initializer = '{}' + it('works for function returns', () => { const [filePath, fileContent] = FsMocker.instance.addFile(/* ts */ ` interface TargetType { greeting: string name: string } - class MyClass { - constructor(target: TargetType) {} + function myFunc(): TargetType { + return {} } - - new MyClass(${initializer}) `) const newText = getNewText({ filePath, - initializerPos: getNodeRange(fileContent, initializer, { index: 1 }), + initializerPos: getNodeRange(fileContent, '{}', { index: 0 }), + errorPos: getNodeRange(fileContent, 'return {}'), }) expect(newText).toMatchInitializer({ greeting: 'todo', name: 'todo' }) }) - it('works for method calls', () => { - const initializer = '{}' - const [filePath, fileContent] = FsMocker.instance.addFile(/* ts */ ` - interface TargetType { - greeting: string - name: string - } + describe('classes', () => { + it('works for class constructors', () => { + const initializer = '{}' + const [filePath, fileContent] = FsMocker.instance.addFile(/* ts */ ` + interface TargetType { + greeting: string + name: string + } + + class MyClass { + constructor(target: TargetType) {} + } + + new MyClass(${initializer}) + `) - class MyClass { - public method(targetType: TargetType) {} - } + const newText = getNewText({ + filePath, + initializerPos: getNodeRange(fileContent, initializer, { index: 1 }), + }) - new MyClass().method(${initializer}) - `) + expect(newText).toMatchInitializer({ greeting: 'todo', name: 'todo' }) + }) - const newText = getNewText({ - filePath, - initializerPos: getNodeRange(fileContent, initializer, { index: 1 }), + it('works for method calls', () => { + const initializer = '{}' + const [filePath, fileContent] = FsMocker.instance.addFile(/* ts */ ` + interface TargetType { + greeting: string + name: string + } + + class MyClass { + public method(targetType: TargetType) {} + } + + new MyClass().method(${initializer}) + `) + + const newText = getNewText({ + filePath, + initializerPos: getNodeRange(fileContent, initializer, { index: 1 }), + }) + + expect(newText).toMatchInitializer({ greeting: 'todo', name: 'todo' }) }) - expect(newText).toMatchInitializer({ greeting: 'todo', name: 'todo' }) - }) + it('works for method returns', () => { + const [filePath, fileContent] = FsMocker.instance.addFile(/* ts */ ` + interface TargetType { + greeting: string + name: string + } + + class MyClass { + public myMethod(): TargetType { + return {} + } + } + `) - it.todo('works for function and method returns') + const newText = getNewText({ + filePath, + initializerPos: getNodeRange(fileContent, '{}', { index: 0 }), + errorPos: getNodeRange(fileContent, 'return {}'), + }) + + expect(newText).toMatchInitializer({ greeting: 'todo', name: 'todo' }) + }) + }) describe('scope', () => { it('can use locals that are in scope', () => { diff --git a/packages/plugin/src/code-fixes/declare-missing-object-members.ts b/packages/plugin/src/code-fixes/declare-missing-object-members.ts index 936d9ec..807b40d 100644 --- a/packages/plugin/src/code-fixes/declare-missing-object-members.ts +++ b/packages/plugin/src/code-fixes/declare-missing-object-members.ts @@ -57,6 +57,10 @@ export namespace DeclareMissingObjectMembers { return errorNode } + if (ts.isReturnStatement(errorNode)) { + return TSH.cast(errorNode.expression, ts.isObjectLiteralExpression) + } + throw new Error(`Unhandled errorNode type ${ts.SyntaxKind[errorNode.kind]}`) } @@ -73,7 +77,7 @@ export namespace DeclareMissingObjectMembers { const previousNode = relevantNodes[0] const node = previousNode.parent - if (ts.isVariableDeclaration(node)) { + if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) { const identifier = TSH.cast(node.name, ts.isIdentifier) return { relevantNodes, topLevelSymbol: TSH.deref(ts, typeChecker, identifier) } } @@ -97,7 +101,7 @@ export namespace DeclareMissingObjectMembers { relevantNodes.unshift(node) } - throw new Error('Could find first related type declaration') + throw new Error('Could not find first related type declaration') } function deriveExpectedSymbolFromRelatedNodes( @@ -128,6 +132,12 @@ export namespace DeclareMissingObjectMembers { } if (ts.isFunctionLike(trackedDeclaration)) { + if (ts.isBlock(node)) return trackedSymbol + + if (ts.isReturnStatement(node)) { + return TSH.deref(ts, typeChecker, trackedDeclaration.type) + } + return TSH.getTypeForCallArgument(ts, typeChecker, trackedDeclaration, node) } diff --git a/test-environment/declare-missing-members/constructor-argument-members.ts b/test-environment/declare-missing-members/constructor-argument-members.ts index 3a44038..3290491 100644 --- a/test-environment/declare-missing-members/constructor-argument-members.ts +++ b/test-environment/declare-missing-members/constructor-argument-members.ts @@ -2,7 +2,9 @@ class HttpClient { constructor(args: { timeout: number; baseUrl: string; operation: string }) {} - public makeRequest(args: { method: string, endpoint: string }) {} + public makeRequest(args: { method: string, endpoint: string }): { status: string } { + return {} + } } const baseUrl = 'https://www.example.com'