Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quick fix for functions lacking return expressions #26434

Merged
merged 28 commits into from
Apr 2, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3f59aa9
stash
Kingwl Aug 10, 2018
7671221
add surmise for return type
Kingwl Aug 14, 2018
0b1e6cc
add support for more case
Kingwl Aug 14, 2018
1f9b9c0
add more test case
Kingwl Aug 16, 2018
aca1722
add more testcase and fix all test
Kingwl Aug 20, 2018
7cfd0fb
Merge branch 'master' into returnValueSurmise
Kingwl Apr 29, 2019
cbe59bb
fix changed diagnosis
Kingwl Apr 30, 2019
48f1cca
Merge branch 'master' into returnValueSurmise
Kingwl May 8, 2019
98d7eba
fix broken test case
Kingwl May 8, 2019
537e806
add more case
Kingwl May 8, 2019
505e299
Merge branch 'master' into returnValueSurmise
Kingwl Jan 9, 2020
6fecf32
rename quickfix
Kingwl Jan 9, 2020
4c0af72
fix conflict
Kingwl Jan 9, 2020
98377f3
fix fix desc
Kingwl Jan 9, 2020
49d21bf
fix semi
Kingwl Jan 9, 2020
a2b15ed
Merge branch 'master' into returnValueSurmise
Kingwl Mar 4, 2020
eabc5a4
Merge branch 'master' into returnValueSurmise
Kingwl Mar 4, 2020
944ddf4
Avoid replace brace with paren
Kingwl Mar 18, 2020
6c88a01
Split fix all action
Kingwl Mar 18, 2020
94f845a
Add return work in same line
Kingwl Mar 18, 2020
a7f37a3
Merge branch 'master' into returnValueSurmise
Kingwl Mar 18, 2020
7fe9a82
fix test cases
Kingwl Mar 18, 2020
af9552e
rename baseline
Kingwl Mar 26, 2020
601fc5e
refactor and handle comment
Kingwl Mar 26, 2020
175cf4e
Support semi
Kingwl Mar 26, 2020
8b332fe
Merge branch 'master' into returnValueSurmise
Kingwl Mar 26, 2020
5d8355c
make helper internal
Kingwl Mar 26, 2020
522cac8
Merge branch 'master' into returnValueSurmise
Kingwl Apr 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,9 +574,7 @@ namespace ts {
},
getApparentType,
getUnionType,
isTypeAssignableTo: (source, target) => {
return isTypeAssignableTo(source, target);
},
isTypeAssignableTo,
createAnonymousType,
createSignature,
createSymbol,
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5553,6 +5553,30 @@
"category": "Message",
"code": 95107
},
"Add a return statement": {
"category": "Message",
"code": 95108
},
"Remove block body braces": {
"category": "Message",
"code": 95109
},
"Wrap this object literal with parentheses": {
Kingwl marked this conversation as resolved.
Show resolved Hide resolved
"category": "Message",
"code": 95110
},
"Add all missing return statement": {
"category": "Message",
"code": 95111
},
"Remove all incorrect body block braces": {
"category": "Message",
"code": 95112
},
"Wrap all object literal with parentheses": {
"category": "Message",
"code": 95113
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
211 changes: 211 additions & 0 deletions src/services/codefixes/returnValueCorrect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/* @internal */
namespace ts.codefix {
const fixId = "returnValueCorrect";
const fixIdAddReturnStatement = "fixAddReturnStatement";
const fixIdRemoveBlockBodyBrace = "fixRemoveBlockBodyBrace";
const fixIdWrapTheBlockWithParen = "fixWrapTheBlockWithParen";
const errorCodes = [
Diagnostics.A_function_whose_declared_type_is_neither_void_nor_any_must_return_a_value.code,
Diagnostics.Type_0_is_not_assignable_to_type_1.code,
Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code
];

enum FixKind {
MissingReturnStatement,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, these are problem kinds. The corresponding fix kinds would be "AddReturnStatement" and "AddParentheses".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there only two kinds of fixes? Shouldn't there also be one for removing the braces?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why not just use the fix IDs from above?

MissingParentheses
}

interface MissingReturnInfo {
kind: FixKind.MissingReturnStatement;
declaration: FunctionLikeDeclaration;
expression: Expression;
statement: Statement;
}

interface MissingParenInfo {
kind: FixKind.MissingParentheses;
declaration: ArrowFunction;
expression: Expression;
statement: Statement;
}

type Info = MissingReturnInfo | MissingParenInfo;

registerCodeFix({
errorCodes,
fixIds: [fixIdAddReturnStatement, fixIdRemoveBlockBodyBrace, fixIdWrapTheBlockWithParen],
getCodeActions: context => {
const { program, sourceFile, span: { start }, errorCode } = context;
const info = getInfo(program.getTypeChecker(), sourceFile, start, errorCode);
if (!info) return undefined;

if (info.kind === FixKind.MissingReturnStatement) {
return append(
[getActionForfixAddReturnStatement(context, info.expression, info.statement)],
isArrowFunction(info.declaration) ? getActionForfixRemoveBlockBodyBrace(context, info.declaration, info.expression): undefined);
}
else {
return [getActionForfixWrapTheBlockWithParen(context, info.declaration, info.expression)];
}
},
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const info = getInfo(context.program.getTypeChecker(), diag.file, diag.start, diag.code);
if (!info) return undefined;

switch (context.fixId) {
case fixIdAddReturnStatement:
addReturnStatement(changes, diag.file, info.expression, info.statement);
break;
case fixIdRemoveBlockBodyBrace:
if (!isArrowFunction(info.declaration)) return undefined;
removeBlockBodyBrace(changes, diag.file, info.declaration, info.expression, /* withParen */ false);
break;
case fixIdWrapTheBlockWithParen:
if (!isArrowFunction(info.declaration)) return undefined;
wrapBlockWithParen(changes, diag.file, info.declaration, info.expression);
break;
default:
Debug.fail(JSON.stringify(context.fixId));
}
}),
});

function updateFunctionLikeBody(declaration: FunctionLikeDeclaration, body: Block): FunctionLikeDeclaration {
Kingwl marked this conversation as resolved.
Show resolved Hide resolved
switch (declaration.kind) {
case SyntaxKind.FunctionDeclaration:
return createFunctionDeclaration(declaration.decorators, declaration.modifiers, declaration.asteriskToken, declaration.name, declaration.typeParameters, declaration.parameters, declaration.type, body);
case SyntaxKind.MethodDeclaration:
return createMethod(declaration.decorators, declaration.modifiers, declaration.asteriskToken, declaration.name, declaration.questionToken, declaration.typeParameters, declaration.parameters, declaration.type, body);
case SyntaxKind.GetAccessor:
return createGetAccessor(declaration.decorators, declaration.modifiers, declaration.name, declaration.parameters, declaration.type, body);
case SyntaxKind.SetAccessor:
return createSetAccessor(declaration.decorators, declaration.modifiers, declaration.name, declaration.parameters, body);
case SyntaxKind.Constructor:
return createConstructor(declaration.decorators, declaration.modifiers, declaration.parameters, body);
case SyntaxKind.FunctionExpression:
return createFunctionExpression(declaration.modifiers, declaration.asteriskToken, declaration.name, declaration.typeParameters, declaration.parameters, declaration.type, body);
case SyntaxKind.ArrowFunction:
return createArrowFunction(declaration.modifiers, declaration.typeParameters, declaration.parameters, declaration.type, declaration.equalsGreaterThanToken, body);
}
}

function getFixInfo(checker: TypeChecker, declaration: FunctionLikeDeclaration, expectType: Type, isFunctionType: boolean): Info | undefined {
if (!declaration.body || !isBlock(declaration.body) || length(declaration.body.statements) !== 1) return undefined;

const firstStatement = first(declaration.body.statements);
if (isExpressionStatement(firstStatement) && checkFixedAssignableTo(checker, declaration, firstStatement.expression, expectType, isFunctionType)) {
return {
declaration,
kind: FixKind.MissingReturnStatement,
expression: firstStatement.expression,
statement: firstStatement
};
}
else if (isLabeledStatement(firstStatement) && isExpressionStatement(firstStatement.statement)) {
const node = createObjectLiteral([createPropertyAssignment(firstStatement.label, firstStatement.statement.expression)]);
if (checkFixedAssignableTo(checker, declaration, node, expectType, isFunctionType)) {
return isArrowFunction(declaration) ? {
declaration,
kind: FixKind.MissingParentheses,
expression: node,
statement: firstStatement
} : {
declaration,
kind: FixKind.MissingReturnStatement,
expression: node,
statement: firstStatement
};
}
}
else if (isBlock(firstStatement) && length(firstStatement.statements) === 1) {
const firstBlockStatement = first(firstStatement.statements);
if (isLabeledStatement(firstBlockStatement) && isExpressionStatement(firstBlockStatement.statement)) {
const node = createObjectLiteral([createPropertyAssignment(firstBlockStatement.label, firstBlockStatement.statement.expression)]);
if (checkFixedAssignableTo(checker, declaration, node, expectType, isFunctionType)) {
return {
declaration,
kind: FixKind.MissingReturnStatement,
expression: node,
statement: firstStatement
};
}
}
}

return undefined;
}

function checkFixedAssignableTo(checker: TypeChecker, declaration: FunctionLikeDeclaration, expr: Expression, type: Type, isFunctionType: boolean) {
return checker.isTypeAssignableTo(checker.getTypeAtLocation(isFunctionType ? updateFunctionLikeBody(declaration, createBlock([createReturn(expr)])) : expr), type);
}

function getInfo(checker: TypeChecker, sourceFile: SourceFile, position: number, errorCode: number): Info | undefined {
const node = getTokenAtPosition(sourceFile, position);
if (!node.parent) return undefined;

const declaration = findAncestor(node.parent, isFunctionLikeDeclaration);
switch (errorCode) {
case Diagnostics.A_function_whose_declared_type_is_neither_void_nor_any_must_return_a_value.code:
if (!declaration || !declaration.body || !declaration.type || !rangeContainsRange(declaration.type, node)) return undefined;
return getFixInfo(checker, declaration, checker.getTypeFromTypeNode(declaration.type), /* isFunctionType */ false);
case Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code:
if (!declaration || !isCallExpression(declaration.parent) || !declaration.body) return undefined;
const pos = declaration.parent.arguments.indexOf(<Expression>declaration);
const type = checker.getContextualTypeForArgumentAtIndex(declaration.parent, pos);
if (!type) return undefined;
return getFixInfo(checker, declaration, type, /* isFunctionType */ true);
case Diagnostics.Type_0_is_not_assignable_to_type_1.code:
if (!isDeclarationName(node) || !isVariableLike(node.parent) && !isJsxAttribute(node.parent)) return undefined;
const initializer = getVariableLikeInitializer(node.parent);
if (!initializer || !isFunctionLikeDeclaration(initializer) || !initializer.body) return undefined;
return getFixInfo(checker, initializer, checker.getTypeAtLocation(node.parent), /* isFunctionType */ true);
}
return undefined;
}

function getVariableLikeInitializer(declaration: VariableLikeDeclaration): Expression | undefined {
switch (declaration.kind) {
case SyntaxKind.VariableDeclaration:
case SyntaxKind.Parameter:
case SyntaxKind.BindingElement:
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertyAssignment:
return declaration.initializer;
case SyntaxKind.JsxAttribute:
return declaration.initializer && (isJsxExpression(declaration.initializer) ? declaration.initializer.expression : undefined);
case SyntaxKind.ShorthandPropertyAssignment:
case SyntaxKind.PropertySignature:
case SyntaxKind.EnumMember:
case SyntaxKind.JSDocPropertyTag:
case SyntaxKind.JSDocParameterTag:
return undefined;
}
}

function addReturnStatement(changes: textChanges.ChangeTracker, sourceFile: SourceFile, expression: Expression, statement: Statement) {
changes.replaceNode(sourceFile, statement, createReturn(expression));
}

function removeBlockBodyBrace(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: ArrowFunction, expression: Expression, withParen: boolean) {
changes.replaceNode(sourceFile, declaration.body, (withParen || needsParentheses(expression)) ? createParen(expression) : expression);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to delete comments? I'd be worried about anything between the end of the expression and the closing brace (and maybe things before the opening brace).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it might delete {/*this comment*/1}. Not the end of the world.

}

function wrapBlockWithParen(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: ArrowFunction, expression: Expression) {
changes.replaceNode(sourceFile, declaration.body, createParen(expression));
}

function getActionForfixAddReturnStatement(context: CodeFixContext, expression: Expression, statement: Statement) {
const changes = textChanges.ChangeTracker.with(context, t => addReturnStatement(t, context.sourceFile, expression, statement));
return createCodeFixAction(fixId, changes, Diagnostics.Add_a_return_statement, fixIdAddReturnStatement, Diagnostics.Add_all_missing_return_statement);
}

function getActionForfixRemoveBlockBodyBrace(context: CodeFixContext, declaration: ArrowFunction, expression: Expression) {
const changes = textChanges.ChangeTracker.with(context, t => removeBlockBodyBrace(t, context.sourceFile, declaration, expression, /* withParen */ false));
return createCodeFixAction(fixId, changes, Diagnostics.Remove_block_body_braces, fixIdRemoveBlockBodyBrace, Diagnostics.Remove_all_incorrect_body_block_braces);
}

function getActionForfixWrapTheBlockWithParen(context: CodeFixContext, declaration: ArrowFunction, expression: Expression) {
const changes = textChanges.ChangeTracker.with(context, t => wrapBlockWithParen(t, context.sourceFile, declaration, expression));
return createCodeFixAction(fixId, changes, Diagnostics.Wrap_this_object_literal_with_parentheses, fixIdWrapTheBlockWithParen, Diagnostics.Wrap_all_object_literal_with_parentheses);
}
}
4 changes: 0 additions & 4 deletions src/services/refactors/addOrRemoveBracesToArrowFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
return { renameFilename: undefined, renameLocation: undefined, edits };
}

function needsParentheses(expression: Expression) {
return isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken || isObjectLiteralExpression(expression);
}

function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number): Info | undefined {
const node = getTokenAtPosition(file, startPosition);
const func = getContainingFunction(node);
Expand Down
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"codefixes/importFixes.ts",
"codefixes/fixImplicitThis.ts",
"codefixes/fixSpelling.ts",
"codefixes/returnValueCorrect.ts",
"codefixes/fixAddMissingMember.ts",
"codefixes/fixAddMissingNewOperator.ts",
"codefixes/fixCannotFindModule.ts",
Expand Down
5 changes: 5 additions & 0 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,11 @@ namespace ts {
return idx === -1 ? -1 : idx + 1;
}

/* @internal */
export function needsParentheses(expression: Expression) {
return isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken || isObjectLiteralExpression(expression);
}

export function getContextualTypeFromParent(node: Expression, checker: TypeChecker): Type | undefined {
const { parent } = node;
switch (parent.kind) {
Expand Down
3 changes: 2 additions & 1 deletion tests/cases/fourslash/codeFixAwaitInSyncFunction10.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
////}

verify.codeFix({
index: 2,
description: "Add async modifier to containing function",
newFileContent:
`const f: () => Promise<number | string> = async () => {
await Promise.resolve('foo');
}`,
}`
});
3 changes: 2 additions & 1 deletion tests/cases/fourslash/codeFixAwaitInSyncFunction11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

// should not change type if it's incorrectly set
verify.codeFix({
index: 2,
description: "Add async modifier to containing function",
newFileContent:
`const f: string = async () => {
await Promise.resolve('foo');
}`,
}`
});
1 change: 1 addition & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
////}

verify.codeFix({
index: 1,
description: "Add async modifier to containing function",
newFileContent:
`const f: () => Promise<Array<number | string>> = async function() {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
////}

verify.codeFix({
index: 2,
description: "Add async modifier to containing function",
newFileContent:
`const f: () => Promise<number | string> = async () => {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction14.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
////}

verify.codeFix({
index: 1,
description: "Add async modifier to containing function",
newFileContent:
`const f = async function(): Promise<number> {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction15.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
////}

verify.codeFix({
index: 2,
description: "Add async modifier to containing function",
newFileContent:
`const f = async (): Promise<number[]> => {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
////}

verify.codeFix({
index: 1,
description: "Add async modifier to containing function",
newFileContent:
`async function f(): Promise<number | string> {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/fourslash/codeFixAwaitInSyncFunction9.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
////}

verify.codeFix({
index: 1,
description: "Add async modifier to containing function",
newFileContent:
`class Foo {
Expand Down
9 changes: 9 additions & 0 deletions tests/cases/fourslash/codeFixSurmiseReturnValue1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
Kingwl marked this conversation as resolved.
Show resolved Hide resolved

//// function Foo (): number {
//// 1
//// }

verify.codeFixAvailable([
{ description: 'Add a return statement' },
]);
8 changes: 8 additions & 0 deletions tests/cases/fourslash/codeFixSurmiseReturnValue10.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// <reference path='fourslash.ts' />

//// const a: ((() => number) | (() => undefined)) = () => { 1 }

verify.codeFixAvailable([
{ description: 'Add a return statement' },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a statement has proven to be non-trivial in the past because people have strong feelings about semicolon usage. Personally, I think I'd only offer that fix if the expression to be returned were on its own line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think @andrewbranch did a bunch of work to make semicolons match the surrounding style automatically. 😄

{ description: 'Remove block body braces' }
]);
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixSurmiseReturnValue11.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />
//// interface A {
//// bar: string
//// }
////
//// function Foo (): A {
//// { bar: '123' }
//// }

verify.codeFixAvailable([
{ description: 'Add a return statement' },
amcasey marked this conversation as resolved.
Show resolved Hide resolved
{ description: 'Remove unused label' },
]);
Loading