Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,258 changes: 1,258 additions & 0 deletions its/ruling/src/test/resources/expected/python-S3776.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions its/ruling/src/test/resources/profile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,10 @@
<key>S1764</key>
<priority>INFO</priority>
</rule>
<rule>
<repositoryKey>python</repositoryKey>
<key>S3776</key>
<priority>INFO</priority>
</rule>
</rules>
</profile>
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ private CheckList() {

public static Iterable<Class> getChecks() {
return ImmutableList.<Class>of(
CognitiveComplexityFunctionCheck.class,
ParsingErrorCheck.class,
CommentRegularExpressionCheck.class,
LineLengthCheck.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* SonarQube Python Plugin
* Copyright (C) 2011-2017 SonarSource SA
* mailto:info 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.python.checks;

import com.sonar.sslr.api.AstNode;
import java.util.HashSet;
import java.util.Set;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.python.PythonCheck;
import org.sonar.python.api.PythonGrammar;
import org.sonar.python.api.PythonKeyword;
import org.sonar.python.api.PythonPunctuator;
import org.sonar.squidbridge.annotations.ActivatedByDefault;
import org.sonar.squidbridge.annotations.SqaleLinearWithOffsetRemediation;

@Rule(
key = CognitiveComplexityFunctionCheck.CHECK_KEY,
name = "Cognitive Complexity of functions should not be too high",
priority = Priority.CRITICAL,
tags = Tags.BRAIN_OVERLOAD
)
@ActivatedByDefault
@SqaleLinearWithOffsetRemediation(
coeff = "1min",
offset = "5min",
effortToFixDescription = "per complexity point above the threshold")
public class CognitiveComplexityFunctionCheck extends PythonCheck {

private static final String MESSAGE = "Refactor this method to reduce its Cognitive Complexity from %s to the %s allowed.";
public static final String CHECK_KEY = "S3776";
private static final int DEFAULT_THRESHOLD = 15;

private AstNode currentFunction = null;
private int complexity;
private int nestingLevel;
private Set<IssueLocation> secondaryLocations = new HashSet<>();

@RuleProperty(
key = "threshold",
description = "The maximum authorized complexity.",
defaultValue = "" + DEFAULT_THRESHOLD)
private int threshold = DEFAULT_THRESHOLD;

public void setThreshold(int threshold) {
this.threshold = threshold;
}

@Override
public void init() {
subscribeTo(
PythonGrammar.IF_STMT,
PythonKeyword.ELIF,
PythonKeyword.ELSE,

PythonGrammar.WHILE_STMT,
PythonGrammar.FOR_STMT,
PythonGrammar.EXCEPT_CLAUSE,

PythonGrammar.AND_TEST,
PythonGrammar.OR_TEST,

PythonGrammar.TEST,

PythonGrammar.FUNCDEF,
PythonGrammar.SUITE);
}

@Override
public void visitNode(AstNode astNode) {
if (astNode.is(PythonGrammar.FUNCDEF) && currentFunction == null) {
currentFunction = astNode;
complexity = 0;
nestingLevel = 0;
secondaryLocations.clear();
}

if (currentFunction != null) {
if (astNode.is(PythonGrammar.SUITE) && incrementsNestingLevel(astNode)) {
nestingLevel++;
}

checkComplexity(astNode);
}
}

private void checkComplexity(AstNode astNode) {
if (astNode.is(PythonGrammar.IF_STMT, PythonGrammar.WHILE_STMT, PythonGrammar.FOR_STMT, PythonGrammar.EXCEPT_CLAUSE)) {
incrementWithNesting(astNode.getFirstChild());
}

if (astNode.is(PythonKeyword.ELIF) || (astNode.is(PythonKeyword.ELSE) && astNode.getNextSibling().is(PythonPunctuator.COLON))) {
incrementWithoutNesting(astNode);
}

if (astNode.is(PythonGrammar.AND_TEST, PythonGrammar.OR_TEST)) {
incrementWithoutNesting(astNode.getFirstChild(PythonKeyword.AND, PythonKeyword.OR));
}

// conditional expression
if (astNode.is(PythonGrammar.TEST) && astNode.hasDirectChildren(PythonKeyword.IF)) {
incrementWithNesting(astNode.getFirstChild(PythonKeyword.IF));
}
}

@Override
public void leaveNode(AstNode astNode) {
if (currentFunction == null) {
return;
}

if (currentFunction.equals(astNode)) {
if (complexity > threshold){
raiseIssue();
}
currentFunction = null;
}

if (astNode.is(PythonGrammar.SUITE) && incrementsNestingLevel(astNode)) {
nestingLevel--;
}
}

private void raiseIssue() {
String message = String.format(MESSAGE, complexity, threshold);
PreciseIssue issue = addIssue(currentFunction.getFirstChild(PythonGrammar.FUNCNAME), message)
.withCost(complexity - threshold);
secondaryLocations.forEach(issue::secondary);
}

private boolean incrementsNestingLevel(AstNode astNode) {
AstNode previousSibling = astNode.getPreviousSibling().getPreviousSibling();
if (previousSibling.is(PythonKeyword.TRY, PythonKeyword.FINALLY)) {
return false;
}

AstNode parent = astNode.getParent();

return !parent.is(PythonGrammar.WITH_STMT, PythonGrammar.CLASSDEF)
&& (!parent.is(PythonGrammar.FUNCDEF) || !parent.equals(currentFunction));
}

private void incrementWithNesting(AstNode secondaryLocationNode) {
int currentNodeComplexity = nestingLevel + 1;
incrementComplexity(secondaryLocationNode, currentNodeComplexity);
}

private void incrementWithoutNesting(AstNode secondaryLocationNode) {
incrementComplexity(secondaryLocationNode, 1);
}

private void incrementComplexity(AstNode secondaryLocationNode, int currentNodeComplexity) {
secondaryLocations.add(new IssueLocation(secondaryLocationNode, secondaryMessage(currentNodeComplexity)));
complexity += currentNodeComplexity;
}

private static String secondaryMessage(int complexity) {
if (complexity == 1) {
return "+1";

} else{
return String.format("+%s (incl %s for nesting)", complexity, complexity - 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena Can we test this secondary message? It is currently not done in the test file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ivandalbosco No, it's not implemented in python, I've tested line numbers only

}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.sonar.python.PythonCheck;
import org.sonar.python.api.PythonGrammar;
import org.sonar.python.api.PythonMetric;
import org.sonar.squidbridge.annotations.ActivatedByDefault;
import org.sonar.squidbridge.annotations.SqaleLinearWithOffsetRemediation;
import org.sonar.squidbridge.api.SourceFunction;

Expand All @@ -40,7 +39,6 @@
coeff = "1min",
offset = "10min",
effortToFixDescription = "per complexity point above the threshold")
@ActivatedByDefault
public class FunctionComplexityCheck extends PythonCheck {
private static final int DEFAULT_MAXIMUM_FUNCTION_COMPLEXITY_THRESHOLD = 15;
private static final String MESSAGE = "Function has a complexity of %s which is greater than %s authorized.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>Cognitive Complexity is a measure of how hard the control flow of a function is to understand. Functions with high Cognitive Complexity will be
difficult to maintain.</p>
<h2>See</h2>
<ul>
<li> <a href="http://redirect.sonarsource.com/doc/cognitive-complexity.html">Cognitive Complexity</a> </li>
</ul>

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SonarQube Python Plugin
* Copyright (C) 2011-2017 SonarSource SA
* mailto:info 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.python.checks;

import org.junit.Test;
import org.sonar.python.checks.utils.PythonCheckVerifier;

public class CognitiveComplexityFunctionCheckTest {

private final CognitiveComplexityFunctionCheck check = new CognitiveComplexityFunctionCheck();

@Test
public void test() {
check.setThreshold(0);
PythonCheckVerifier.verify("src/test/resources/checks/cognitiveComplexityFunction.py", check);
}

@Test
public void default_threshold() throws Exception {
PythonCheckVerifier.verify("src/test/resources/checks/cognitiveComplexityFunctionDefault.py", check);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static void verifyIssue(TestIssue expected, PreciseIssue actual) {
assertThat(actual.primaryLocation().message()).as("Bad message at line " + expected.line()).isEqualTo(expected.message());
}
if (expected.effortToFix() != null) {
assertThat(actual.cost()).as("Bad effortToFix at line " + expected.line()).isEqualTo(expected.effortToFix());
assertThat(actual.cost().intValue()).as("Bad effortToFix at line " + expected.line()).isEqualTo(expected.effortToFix());
}
if (expected.startColumn() != null) {
assertThat(actual.primaryLocation().startLineOffset() + 1).as("Bad start column at line " + expected.line()).isEqualTo(expected.startColumn());
Expand Down
Loading