Quick fix for functions lacking return expressions (#26434)

* stash

* add surmise for return type

* add support for more case

* add more test case

* add more testcase and fix all test

* fix changed diagnosis

* fix broken test case

* add more case

* rename quickfix

* fix conflict

* fix fix desc

* fix semi

* Avoid replace brace with paren

* Split fix all action

* Add return work in same line

* fix test cases

* rename baseline

* refactor and handle comment

* Support semi

* make helper internal
This commit is contained in:
Wenlu Wang
2020-04-03 01:06:14 +08:00
committed by GitHub
parent 54b0e4acc5
commit afc41f095d
45 changed files with 947 additions and 30 deletions

View File

@@ -574,9 +574,7 @@ namespace ts {
},
getApparentType,
getUnionType,
isTypeAssignableTo: (source, target) => {
return isTypeAssignableTo(source, target);
},
isTypeAssignableTo,
createAnonymousType,
createSignature,
createSymbol,

View File

@@ -5621,6 +5621,31 @@
"category": "Message",
"code": 95110
},
"Add a return statement": {
"category": "Message",
"code": 95111
},
"Remove block body braces": {
"category": "Message",
"code": 95112
},
"Wrap the following body with parentheses which should be an object literal": {
"category": "Message",
"code": 95113
},
"Add all missing return statement": {
"category": "Message",
"code": 95114
},
"Remove all incorrect body block braces": {
"category": "Message",
"code": 95115
},
"Wrap all object literal with parentheses": {
"category": "Message",
"code": 95116
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
"code": 18004

View File

@@ -2057,6 +2057,26 @@ namespace ts {
: node;
}
/* @internal */
export function updateFunctionLikeBody(declaration: FunctionLikeDeclaration, body: Block): FunctionLikeDeclaration {
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);
}
}
export function createClassDeclaration(
decorators: readonly Decorator[] | undefined,
modifiers: readonly Modifier[] | undefined,

View File

@@ -0,0 +1,208 @@
/* @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 ProblemKind {
MissingReturnStatement,
MissingParentheses
}
interface MissingReturnInfo {
kind: ProblemKind.MissingReturnStatement;
declaration: FunctionLikeDeclaration;
expression: Expression;
statement: Statement;
commentSource: Node;
}
interface MissingParenInfo {
kind: ProblemKind.MissingParentheses;
declaration: ArrowFunction;
expression: Expression;
statement: Statement;
commentSource: Node;
}
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 === ProblemKind.MissingReturnStatement) {
return append(
[getActionForfixAddReturnStatement(context, info.expression, info.statement)],
isArrowFunction(info.declaration) ? getActionForfixRemoveBlockBodyBrace(context, info.declaration, info.expression, info.commentSource): 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, info.commentSource, /* 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 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: ProblemKind.MissingReturnStatement,
expression: firstStatement.expression,
statement: firstStatement,
commentSource: firstStatement.expression
};
}
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: ProblemKind.MissingParentheses,
expression: node,
statement: firstStatement,
commentSource: firstStatement.statement.expression
} : {
declaration,
kind: ProblemKind.MissingReturnStatement,
expression: node,
statement: firstStatement,
commentSource: firstStatement.statement.expression
};
}
}
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: ProblemKind.MissingReturnStatement,
expression: node,
statement: firstStatement,
commentSource: firstBlockStatement
};
}
}
}
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) {
suppressLeadingAndTrailingTrivia(expression);
const probablyNeedSemi = probablyUsesSemicolons(sourceFile);
changes.replaceNode(sourceFile, statement, createReturn(expression), {
leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude,
trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude,
suffix: probablyNeedSemi ? ";" : undefined
});
}
function removeBlockBodyBrace(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: ArrowFunction, expression: Expression, commentSource: Node, withParen: boolean) {
const newBody = (withParen || needsParentheses(expression)) ? createParen(expression) : expression;
suppressLeadingAndTrailingTrivia(commentSource);
copyComments(commentSource, newBody);
changes.replaceNode(sourceFile, declaration.body, newBody);
}
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, commentSource: Node) {
const changes = textChanges.ChangeTracker.with(context, t => removeBlockBodyBrace(t, context.sourceFile, declaration, expression, commentSource, /* 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_the_following_body_with_parentheses_which_should_be_an_object_literal, fixIdWrapTheBlockWithParen, Diagnostics.Wrap_all_object_literal_with_parentheses);
}
}

View File

@@ -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);

View File

@@ -491,27 +491,6 @@ namespace ts.refactor.convertParamsToDestructuredObject {
}
}
function copyComments(sourceNode: Node, targetNode: Node) {
const sourceFile = sourceNode.getSourceFile();
const text = sourceFile.text;
if (hasLeadingLineBreak(sourceNode, text)) {
copyLeadingComments(sourceNode, targetNode, sourceFile);
}
else {
copyTrailingAsLeadingComments(sourceNode, targetNode, sourceFile);
}
copyTrailingComments(sourceNode, targetNode, sourceFile);
}
function hasLeadingLineBreak(node: Node, text: string) {
const start = node.getFullStart();
const end = node.getStart();
for (let i = start; i < end; i++) {
if (text.charCodeAt(i) === CharacterCodes.lineFeed) return true;
}
return false;
}
function getParameterName(paramDeclaration: ValidParameterDeclaration) {
return getTextOfIdentifierOrLiteral(paramDeclaration.name);
}

View File

@@ -65,6 +65,7 @@
"codefixes/importFixes.ts",
"codefixes/fixImplicitThis.ts",
"codefixes/fixSpelling.ts",
"codefixes/returnValueCorrect.ts",
"codefixes/fixAddMissingMember.ts",
"codefixes/fixAddMissingNewOperator.ts",
"codefixes/fixCannotFindModule.ts",

View File

@@ -2273,6 +2273,27 @@ namespace ts {
addEmitFlagsRecursively(node, EmitFlags.NoTrailingComments, getLastChild);
}
export function copyComments(sourceNode: Node, targetNode: Node) {
const sourceFile = sourceNode.getSourceFile();
const text = sourceFile.text;
if (hasLeadingLineBreak(sourceNode, text)) {
copyLeadingComments(sourceNode, targetNode, sourceFile);
}
else {
copyTrailingAsLeadingComments(sourceNode, targetNode, sourceFile);
}
copyTrailingComments(sourceNode, targetNode, sourceFile);
}
function hasLeadingLineBreak(node: Node, text: string) {
const start = node.getFullStart();
const end = node.getStart();
for (let i = start; i < end; i++) {
if (text.charCodeAt(i) === CharacterCodes.lineFeed) return true;
}
return false;
}
function addEmitFlagsRecursively(node: Node, flag: EmitFlags, getChild: (n: Node) => Node | undefined) {
addEmitFlags(node, flag);
const child = getChild(node);
@@ -2367,6 +2388,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) {