/* * SonarQube Java * Copyright (C) 2012-2016 SonarSource SA * mailto:contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.java.checks; import com.google.common.collect.ImmutableList; import org.sonar.check.Rule; import org.sonar.check.RuleProperty; import org.sonar.java.model.ExpressionUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.BreakStatementTree; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.ConditionalExpressionTree; import org.sonar.plugins.java.api.tree.ContinueStatementTree; import org.sonar.plugins.java.api.tree.DoWhileStatementTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.ForEachStatement; import org.sonar.plugins.java.api.tree.ForStatementTree; import org.sonar.plugins.java.api.tree.IfStatementTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.SwitchStatementTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TryStatementTree; import org.sonar.plugins.java.api.tree.WhileStatementTree; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.sonar.plugins.java.api.tree.Tree.Kind.CONDITIONAL_AND; import static org.sonar.plugins.java.api.tree.Tree.Kind.CONDITIONAL_OR; import static org.sonar.plugins.java.api.tree.Tree.Kind.CONSTRUCTOR; import static org.sonar.plugins.java.api.tree.Tree.Kind.IF_STATEMENT; import static org.sonar.plugins.java.api.tree.Tree.Kind.METHOD; @Rule(key = "S3776") public class CognitiveComplexityMethodCheck extends IssuableSubscriptionVisitor { private static final int DEFAULT_MAX = 15; @RuleProperty( key = "Threshold", description = "The maximum authorized complexity.", defaultValue = "" + DEFAULT_MAX) private int max = DEFAULT_MAX; @Override public List<Tree.Kind> nodesToVisit() { return ImmutableList.of(METHOD, CONSTRUCTOR); } @Override public void visitNode(Tree tree) { MethodTree method = (MethodTree) tree; if (method.block() == null || ((ClassTree)method.parent()).simpleName() == null || isWithinLocalClass(method)) { return; } CognitiveComplexityVisitor complexityVisitor = new CognitiveComplexityVisitor(); method.accept(complexityVisitor); int total = complexityVisitor.complexity; if (total > max) { reportIssue(method.simpleName(), "Refactor this method to reduce its Cognitive Complexity from " + total + " to the " + max + " allowed.", complexityVisitor.flow, total - max); } } private boolean isWithinLocalClass(MethodTree method) { return hasSemantic() && method.symbol().owner().owner().isMethodSymbol(); } private static class CognitiveComplexityVisitor extends BaseTreeVisitor { private final List<JavaFileScannerContext.Location> flow; private final Set<Tree> ignored; int complexity; int nesting; boolean ignoreNesting; CognitiveComplexityVisitor() { complexity = 0; nesting = 1; ignoreNesting = false; flow = new ArrayList<>(); ignored = new HashSet<>(); } private void increaseComplexityByNesting(Tree tree) { increaseComplexity(tree, nesting); } private void increaseComplexityByOne(Tree tree) { increaseComplexity(tree, 1); } private void increaseComplexity(Tree tree, int increase) { complexity += increase; if (ignoreNesting) { flow.add(new JavaFileScannerContext.Location("+1", tree)); ignoreNesting = false; } else if (!ignored.contains(tree)) { String message = "+" + increase; if (increase > 1) { message += " (incl " + (increase - 1) + " for nesting)"; } flow.add(new JavaFileScannerContext.Location(message, tree)); } } @Override public void visitIfStatement(IfStatementTree tree) { increaseComplexityByNesting(tree.ifKeyword()); scan(tree.condition()); nesting++; scan(tree.thenStatement()); nesting--; boolean elseStatementNotIF = tree.elseStatement() != null && !tree.elseStatement().is(IF_STATEMENT); if(elseStatementNotIF) { increaseComplexityByOne(tree.elseKeyword()); nesting++; } else if(tree.elseStatement() != null) { // else statement is an if, visiting it will increase complexity by nesting so by one only. ignoreNesting = true; complexity -= nesting - 1; } scan(tree.elseStatement()); if(elseStatementNotIF) { nesting--; } } @Override public void visitTryStatement(TryStatementTree tree) { scan(tree.resources()); scan(tree.block()); tree.catches().forEach(c -> increaseComplexityByNesting(c.catchKeyword())); nesting++; scan(tree.catches()); nesting--; scan(tree.finallyBlock()); } @Override public void visitForStatement(ForStatementTree tree) { increaseComplexityByNesting(tree.forKeyword()); nesting++; super.visitForStatement(tree); nesting--; } @Override public void visitForEachStatement(ForEachStatement tree) { increaseComplexityByNesting(tree.forKeyword()); nesting++; super.visitForEachStatement(tree); nesting--; } @Override public void visitWhileStatement(WhileStatementTree tree) { increaseComplexityByNesting(tree.whileKeyword()); nesting++; super.visitWhileStatement(tree); nesting--; } @Override public void visitDoWhileStatement(DoWhileStatementTree tree) { increaseComplexityByNesting(tree.doKeyword()); nesting++; super.visitDoWhileStatement(tree); nesting--; } @Override public void visitConditionalExpression(ConditionalExpressionTree tree) { increaseComplexityByNesting(tree.questionToken()); nesting++; super.visitConditionalExpression(tree); nesting--; } @Override public void visitSwitchStatement(SwitchStatementTree tree) { increaseComplexityByNesting(tree.switchKeyword()); nesting++; super.visitSwitchStatement(tree); nesting--; } @Override public void visitBreakStatement(BreakStatementTree tree) { if(tree.label() != null) { increaseComplexityByOne(tree.breakKeyword()); } super.visitBreakStatement(tree); } @Override public void visitContinueStatement(ContinueStatementTree tree) { if(tree.label() != null) { increaseComplexityByOne(tree.continueKeyword()); } super.visitContinueStatement(tree); } @Override public void visitClass(ClassTree tree) { nesting++; super.visitClass(tree); nesting--; } @Override public void visitLambdaExpression(LambdaExpressionTree lambdaExpressionTree) { nesting++; super.visitLambdaExpression(lambdaExpressionTree); nesting--; } @Override public void visitBinaryExpression(BinaryExpressionTree tree) { if(tree.is(CONDITIONAL_AND, CONDITIONAL_OR)) { increaseComplexityByOne(tree.operatorToken()); ExpressionTree left = ExpressionUtils.skipParentheses(tree.leftOperand()); if(left.kind() == tree.kind()) { ignored.add(((BinaryExpressionTree) left).operatorToken()); complexity--; } ExpressionTree right = ExpressionUtils.skipParentheses(tree.rightOperand()); if(right.kind() == tree.kind()) { ignored.add(((BinaryExpressionTree) right).operatorToken()); complexity--; } } super.visitBinaryExpression(tree); } } public void setMax(int max) { this.max = max; } }