/*
* Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.goide.inspections;
import com.goide.highlighting.exitpoint.GoBreakStatementExitPointHandler;
import com.goide.psi.*;
import com.goide.psi.impl.GoPsiImplUtil;
import com.intellij.codeInsight.template.Template;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.codeInsight.template.impl.TemplateSettings;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class GoMissingReturnInspection extends GoInspectionBase {
public static final String ADD_RETURN_STATEMENT_QUICK_FIX_NAME = "Add return statement";
private static void check(@Nullable GoSignature signature, @Nullable GoBlock block, @NotNull ProblemsHolder holder) {
if (block == null) return;
GoResult result = signature != null ? signature.getResult() : null;
if (result == null || result.isVoid() || isTerminating(block)) return;
PsiElement brace = block.getRbrace();
holder.registerProblem(brace == null ? block : brace, "Missing return at end of function",
brace == null ? new LocalQuickFix[]{} : new LocalQuickFix[]{new AddReturnFix(block)});
}
// https://tip.golang.org/ref/spec#Terminating_statements
private static boolean isTerminating(@Nullable GoCompositeElement s) {
if (s instanceof GoReturnStatement || s instanceof GoGotoStatement) {
return true;
}
if (s instanceof GoSimpleStatement) {
GoLeftHandExprList list = ((GoSimpleStatement)s).getLeftHandExprList();
GoExpression expression = ContainerUtil.getFirstItem(list != null ? list.getExpressionList() : null);
if (expression instanceof GoCallExpr && GoPsiImplUtil.isPanic((GoCallExpr)expression)) return true;
}
else if (s instanceof GoBlock) {
return isTerminating(ContainerUtil.getLastItem(((GoBlock)s).getStatementList()));
}
else if (s instanceof GoIfStatement) {
GoBlock block = ((GoIfStatement)s).getBlock();
GoStatement st = ((GoIfStatement)s).getElseStatement();
return block != null && isTerminating(block) && st != null && isTerminating(st);
}
else if (s instanceof GoElseStatement) {
GoIfStatement ifStatement = ((GoElseStatement)s).getIfStatement();
if (ifStatement != null) return isTerminating(ifStatement);
GoBlock block = ((GoElseStatement)s).getBlock();
if (block != null) return isTerminating(block);
return false;
}
else if (s instanceof GoForStatement) {
GoForStatement f = (GoForStatement)s;
GoForClause forClause = f.getForClause();
if (forClause != null && forClause.getExpression() != null || f.getExpression() != null || f.getRangeClause() != null) return false;
GoBlock block = f.getBlock();
return block == null || !hasReferringBreakStatement(f);
}
else if (s instanceof GoExprSwitchStatement) {
return isTerminating((GoExprSwitchStatement)s, ((GoExprSwitchStatement)s).getExprCaseClauseList());
}
else if (s instanceof GoTypeSwitchStatement) {
return isTerminating((GoTypeSwitchStatement)s, ((GoTypeSwitchStatement)s).getTypeCaseClauseList());
}
else if (s instanceof GoSelectStatement) {
GoSelectStatement selectStatement = (GoSelectStatement)s;
for (GoCommClause clause : selectStatement.getCommClauseList()) {
List<GoStatement> statements = clause.getStatementList();
if (hasReferringBreakStatement(selectStatement)) return false;
if (!isTerminating(ContainerUtil.getLastItem(statements))) return false;
}
return true;
}
else if (s instanceof GoLabeledStatement) {
GoLabeledStatement labeledStatement = (GoLabeledStatement)s;
return isTerminating(labeledStatement.getStatement());
}
else if (s instanceof GoStatement && ((GoStatement)s).getBlock() != null) {
return isTerminating(((GoStatement)s).getBlock());
}
return false;
}
private static boolean isTerminating(@NotNull GoSwitchStatement switchStatement, @NotNull List<? extends GoCaseClause> clauses) {
boolean hasDefault = false;
for (GoCaseClause clause : clauses) {
hasDefault |= clause.getDefault() != null;
List<GoStatement> statements = clause.getStatementList();
if (hasReferringBreakStatement(switchStatement)) return false;
GoStatement last = ContainerUtil.getLastItem(statements);
if (!(last instanceof GoFallthroughStatement) && !isTerminating(last)) return false;
}
return hasDefault;
}
private static boolean hasReferringBreakStatement(@NotNull PsiElement breakStatementOwner) {
return !GoPsiImplUtil.goTraverser().withRoot(breakStatementOwner).traverse().filter(GoBreakStatement.class).filter(statement -> {
PsiElement owner = GoBreakStatementExitPointHandler.getBreakStatementOwnerOrResolve(statement);
if (breakStatementOwner.equals(owner)) {
return true;
}
if (owner instanceof GoLabelDefinition) {
PsiElement parent = owner.getParent();
if (parent instanceof GoLabeledStatement && breakStatementOwner.equals(((GoLabeledStatement)parent).getStatement())) {
return true;
}
}
return false;
}).isEmpty();
}
@NotNull
@Override
protected GoVisitor buildGoVisitor(@NotNull ProblemsHolder holder, @NotNull LocalInspectionToolSession session) {
return new GoVisitor() {
@Override
public void visitFunctionOrMethodDeclaration(@NotNull GoFunctionOrMethodDeclaration o) { // todo: extract common interface
check(o.getSignature(), o.getBlock(), holder);
}
@Override
public void visitFunctionLit(@NotNull GoFunctionLit o) {
check(o.getSignature(), o.getBlock(), holder);
}
};
}
private static class AddReturnFix extends LocalQuickFixAndIntentionActionOnPsiElement {
public AddReturnFix(@NotNull GoBlock block) {
super(block);
}
@NotNull
@Override
public String getText() {
return ADD_RETURN_STATEMENT_QUICK_FIX_NAME;
}
@NotNull
@Override
public String getFamilyName() {
return getName();
}
@Override
public void invoke(@NotNull Project project,
@NotNull PsiFile file,
@Nullable("is null when called from inspection") Editor editor,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
if (!(file instanceof GoFile) || editor == null || !(startElement instanceof GoBlock)) return;
PsiElement brace = ((GoBlock)startElement).getRbrace();
if (brace == null) return;
Template template = TemplateSettings.getInstance().getTemplateById("go_lang_add_return");
if (template == null) return;
int start = brace.getTextRange().getStartOffset();
editor.getCaretModel().moveToOffset(start);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
template.setToReformat(true);
TemplateManager.getInstance(project).startTemplate(editor, template, true, Collections.emptyMap(), null);
}
}
}