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

SONARJS-862 Rule: Cognitive Complexity of functions should not be too high #449

Merged
merged 9 commits into from
Jan 30, 2017

Conversation

vilchik-elena
Copy link
Contributor

No description provided.

@vilchik-elena vilchik-elena force-pushed the SONARJS-862 branch 3 times, most recently from 1282a32 to c095cf6 Compare January 26, 2017 11:12
Copy link
Contributor

@ivandalbosco ivandalbosco left a comment

Choose a reason for hiding this comment

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

@vilchik-elena Good work! Kindly check my comments, Don't forget to handle the "(a && b) && c" and "a && (b && c)" cases!

}
}

private class CognitiveComplexity extends DoubleDispatchVisitor {
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena This class is a valuable asset of the analyzer. It is likely to be reused in the future, e.g., for computing the complexity of a class or a file, or to display the complexity, etc. Therefore I would like to see this class as a top-level one, in project javascript-fronted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ivandalbosco I would prefer to extract it as soon as it will be required somewhere else.

Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena @pynicolas @inverno Actually we had a long discussion last Friday (unfortunately you weren't present) about this point. Carlo's view point and mine was that whenever a strong business notion is found, it immediately deserves a separate class; waiting for a second usage of the notion is not a good criterion for creating the separate class. Pierre-Yves was of opposite opinion (yours). I really prefer the former.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ivandalbosco This will require time and efforts now, which are investments in the future, hypothetical future. In fact I've not heard that we will do CG metric, moreover I think CG of class or file contradicts to its notion.


for (i = 0; i < length; i++) { // +1 (nesting level +1)
//S ^^^ LOOPS {{+1}}
if (condition) { // +2
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena How about testing a "for" loop within a "for" loop? In my experience it is a common case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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


function nested_conditional_expression() { // Noncompliant [[effortToFix=6]]
x = condition1 ? (condition2 ? trueValue2 : falseValue2) : falseValue1 ; // +3
x = condition1 ? trueValue1 : (condition2 ? trueValue2 : falseValue2) ; // +3
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena OK. I just suggest that you replace this line with:
x = condition1 ? (condition2 ? trueValue2 : falseValue2) : (condition3 ? trueValue3 : falseValue3); // +5

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ivandalbosco I've added it

//S ^^ TRY_CATCH
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena I suggest you also add a case of nested try-catch, e.g,:

function nested_try_catch() { // Noncompliant [[effortToFix=3;id=NESTED_TRY_CATCH]]
try {
if (condition) { // +1 (nesting level +1)
//S ^^ NESTED_TRY_CATCH
try {}
catch (someError) { // +2
//S ^^^^^ NESTED_TRY_CATCH
}
}
} finally {
}
}

Copy link
Contributor Author

@vilchik-elena vilchik-elena Jan 30, 2017

Choose a reason for hiding this comment

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

@ivandalbosco FYI in github comments you can wrap one line code in single backticks and multiline code in triple backticks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ivandalbosco thanks for the case. Added!


for (var i = a && b; a && b; a && b) { // +1(for) +1(&&) +1(&&) +1(&&)
}
}
Copy link
Contributor

@ivandalbosco ivandalbosco Jan 26, 2017

Choose a reason for hiding this comment

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

@vilchik-elena I may be wrong, but I see nowhere in your nesting tests that complexity may increase more than once at a same nesting level. It is definitely worth it, because I know it has been a matter of debate: do we increase complexity once per level or possibly several times (the latter was retained). For example:

function nesting() { // Noncompliant [[effortToFix=5]]
if (condition) { // +1 (+1 for nesting)
if (condition) {} // +2
if (condition) {} // +2 (this one was debated - personally I disagree but this is irrelevant)
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


/* When complexity of nesting function is considered for this check, we ignore all nested in it functions */
private Set<Tree> ignoredNestedFunctions = new HashSet<>();
private final CognitiveComplexity cognitiveComplexity = new CognitiveComplexity();
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena I am not sure that this implementation is fault-tolerant. Class CognitiveComplexity is stateful. If an exception is raised in CognitiveComplexity#visitFunction(tree), the clean up in method CognitiveComplexity#leaveFunction(tree) will not be performed, so the CognitiveComplexity of the first function of the next file may be polluted. How about moving this initialization to line 85?

Copy link
Contributor Author

@vilchik-elena vilchik-elena Jan 30, 2017

Choose a reason for hiding this comment

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

@ivandalbosco Agree about the fact that CognitiveComplexity#leaveFunction(tree) might not be done in case of some exception. But in fact if some exception is raised the entire analysis is stopped so we don't care.
But we might change this behaviour at any moment so I will force initialization of CognitiveComplexity for each file.
Thanks!


function and_or() { // Noncompliant [[effortToFix=7;id=LOGICAL_OPS]]
foo(1 && 2 && 3 && 4); // +1
//S ^^ LOGICAL_OPS {{+1}}
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena Kindly see Ann's email about
var x = (a && b && c); // +1
var x = ((a && b) && c); // +1, not +2
Thanks!

}
}

private void checkComplexity() {
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena This method could be public, named "getComplexity", and not raising issue. I would delegate the threshold and the issue raising to the enclosing Check class.

Copy link
Contributor Author

@vilchik-elena vilchik-elena Jan 30, 2017

Choose a reason for hiding this comment

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

return "+1";

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

Choose a reason for hiding this comment

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

@vilchik-elena How about adding a dot at the end of "incl", that is "incl." ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// ^^
var functionExpression = function(a, b) { return a && b; } // Noncompliant [[effortToFix=1]]
// ^^^^^^^^

Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena Maybe an example with an IIFE would be nice. How about a nasty little case like this?

(function(a) {
if (cond) {}
return a;
})(function(b) {return b + 1})(0);

I don't know what it is supposed to display... Too tired for today...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ivandalbosco It's nasty, but not much interesting: one issue on first line (complexity = 1). But ok, I will add it

boolean leftChildOfSameKind = leftChild.is(javaScriptTree.getKind());
boolean rightChildOfSameKind = rightChild.is(javaScriptTree.getKind());

// For expressions with same-kind operators like "a && (b && c)" we want to have secondary location on leftmost operator

Choose a reason for hiding this comment

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

MAJOR This block of commented-out lines of code should be removed. rule

@vilchik-elena vilchik-elena force-pushed the SONARJS-862 branch 2 times, most recently from 7cbfe22 to 21c4bd5 Compare January 30, 2017 11:00
@SonarTech
Copy link

SonarQube analysis reported 1 issue

  • MAJOR 1 major

Watch the comments in this conversation to review them.

@@ -207,10 +207,13 @@ function with_complexity_after_nested_function() { // Noncompliant [[effortToFix
if (condition) {} // +1
}

function and_or() { // Noncompliant [[effortToFix=7;id=LOGICAL_OPS]]
function and_or() { // Noncompliant [[effortToFix=8;id=LOGICAL_OPS]]
foo(1 && 2 && 3 && 4); // +1
//S ^^ LOGICAL_OPS {{+1}}
Copy link
Contributor

@ivandalbosco ivandalbosco Jan 30, 2017

Choose a reason for hiding this comment

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

@vilchik-elena Great! I can see that
foo(1 && 2 && (3 && 4))
is +1, whereas
foo(1 && 2 && !(3 && 4))
is +2. Can you please add one or more test with the "not" operator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@inverno done

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

private static class ComplexityData {
Copy link
Contributor

Choose a reason for hiding this comment

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

@vilchik-elena Good design, this class was indeed a must. However, you are in the middle of the river: methods complexity() and aggregatedNestedFunctions() are public (probably for future usage), whereas the class itself is private. So Eclipse correctly tells me that those two methods are "never used locally". I still recommend you move classes CognitiveComplexity and ComplexityData to package org.sonar.javascript.metrics. But this is up to you, I won't argue if you disagree...

@vilchik-elena vilchik-elena force-pushed the SONARJS-862 branch 2 times, most recently from 714ba83 to b4f3d50 Compare January 30, 2017 15:23
@vilchik-elena vilchik-elena merged commit 149e36e into master Jan 30, 2017
@vilchik-elena vilchik-elena deleted the SONARJS-862 branch January 31, 2017 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants