262 lines
6.2 KiB
D
262 lines
6.2 KiB
D
// Distributed under the Boost Software License, Version 1.0.
|
|
// (See accompanying file LICENSE_1_0.txt or copy at
|
|
// http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
module dscanner.analysis.cyclomatic_complexity;
|
|
|
|
import dparse.ast;
|
|
import dparse.lexer;
|
|
import dsymbol.scope_ : Scope;
|
|
import dscanner.analysis.base;
|
|
import dscanner.analysis.helpers;
|
|
|
|
import std.format;
|
|
|
|
/// Implements a basic cyclomatic complexity algorithm using the AST.
|
|
///
|
|
/// Issues a warning on functions whenever the cyclomatic complexity of them
|
|
/// passed over a configurable threshold.
|
|
///
|
|
/// The complexity score starts at 1 and is increased each time on
|
|
/// - `if`
|
|
/// - switch `case`
|
|
/// - any loop
|
|
/// - `&&`
|
|
/// - `||`
|
|
/// - `?:` (ternary operator)
|
|
/// - `throw`
|
|
/// - `catch`
|
|
/// - `return`
|
|
/// - `break` (unless in case)
|
|
/// - `continue`
|
|
/// - `goto`
|
|
/// - function literals
|
|
///
|
|
/// See: https://en.wikipedia.org/wiki/Cyclomatic_complexity
|
|
/// Rules based on http://cyvis.sourceforge.net/cyclomatic_complexity.html
|
|
/// and https://github.com/fzipp/gocyclo
|
|
final class CyclomaticComplexityCheck : BaseAnalyzer
|
|
{
|
|
/// Message key emitted when the threshold is reached
|
|
enum string KEY = "dscanner.metric.cyclomatic_complexity";
|
|
/// Human readable message emitted when the threshold is reached
|
|
enum string MESSAGE = "Cyclomatic complexity of this function is %s.";
|
|
mixin AnalyzerInfo!"cyclomatic_complexity";
|
|
|
|
/// Maximum cyclomatic complexity. Once the cyclomatic complexity is greater
|
|
/// than this threshold, a warning is issued.
|
|
///
|
|
/// By default 50 is used as threshold, which is considered almost
|
|
/// unmaintainable / untestable.
|
|
///
|
|
/// For clean development a threshold like 20 can be used instead.
|
|
int maxCyclomaticComplexity;
|
|
|
|
///
|
|
this(string fileName, const(Scope)* sc, bool skipTests = false,
|
|
int maxCyclomaticComplexity = 50)
|
|
{
|
|
super(fileName, sc, skipTests);
|
|
this.maxCyclomaticComplexity = maxCyclomaticComplexity;
|
|
}
|
|
|
|
mixin VisitComplex!IfStatement;
|
|
mixin VisitComplex!CaseStatement;
|
|
mixin VisitComplex!CaseRangeStatement;
|
|
mixin VisitLoop!DoStatement;
|
|
mixin VisitLoop!WhileStatement;
|
|
mixin VisitLoop!ForStatement;
|
|
mixin VisitLoop!ForeachStatement;
|
|
mixin VisitComplex!AndAndExpression;
|
|
mixin VisitComplex!OrOrExpression;
|
|
mixin VisitComplex!TernaryExpression;
|
|
mixin VisitComplex!ThrowExpression;
|
|
mixin VisitComplex!Catch;
|
|
mixin VisitComplex!LastCatch;
|
|
mixin VisitComplex!ReturnStatement;
|
|
mixin VisitComplex!FunctionLiteralExpression;
|
|
mixin VisitComplex!GotoStatement;
|
|
mixin VisitComplex!ContinueStatement;
|
|
|
|
override void visit(const SwitchStatement n)
|
|
{
|
|
inLoop.assumeSafeAppend ~= false;
|
|
scope (exit)
|
|
inLoop.length--;
|
|
n.accept(this);
|
|
}
|
|
|
|
override void visit(const BreakStatement b)
|
|
{
|
|
if (b.label !is Token.init || inLoop[$ - 1])
|
|
complexityStack[$ - 1]++;
|
|
}
|
|
|
|
override void visit(const FunctionDeclaration fun)
|
|
{
|
|
if (!fun.functionBody)
|
|
return;
|
|
|
|
complexityStack.assumeSafeAppend ~= 1;
|
|
inLoop.assumeSafeAppend ~= false;
|
|
scope (exit)
|
|
{
|
|
complexityStack.length--;
|
|
inLoop.length--;
|
|
}
|
|
fun.functionBody.accept(this);
|
|
testComplexity(fun.name.line, fun.name.column);
|
|
}
|
|
|
|
override void visit(const Unittest unittest_)
|
|
{
|
|
if (!skipTests)
|
|
{
|
|
complexityStack.assumeSafeAppend ~= 1;
|
|
inLoop.assumeSafeAppend ~= false;
|
|
scope (exit)
|
|
{
|
|
complexityStack.length--;
|
|
inLoop.length--;
|
|
}
|
|
unittest_.accept(this);
|
|
testComplexity(unittest_.line, unittest_.column);
|
|
}
|
|
}
|
|
|
|
alias visit = BaseAnalyzer.visit;
|
|
private:
|
|
int[] complexityStack = [0];
|
|
bool[] inLoop = [false];
|
|
|
|
void testComplexity(size_t line, size_t column)
|
|
{
|
|
auto complexity = complexityStack[$ - 1];
|
|
if (complexity > maxCyclomaticComplexity)
|
|
{
|
|
addErrorMessage(line, column, KEY, format!MESSAGE(complexity));
|
|
}
|
|
}
|
|
|
|
template VisitComplex(NodeType, int increase = 1)
|
|
{
|
|
override void visit(const NodeType n)
|
|
{
|
|
complexityStack[$ - 1] += increase;
|
|
n.accept(this);
|
|
}
|
|
}
|
|
|
|
template VisitLoop(NodeType, int increase = 1)
|
|
{
|
|
override void visit(const NodeType n)
|
|
{
|
|
inLoop.assumeSafeAppend ~= true;
|
|
scope (exit)
|
|
inLoop.length--;
|
|
complexityStack[$ - 1] += increase;
|
|
n.accept(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
unittest
|
|
{
|
|
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
|
|
import std.stdio : stderr;
|
|
|
|
StaticAnalysisConfig sac = disabledConfig();
|
|
sac.cyclomatic_complexity = Check.enabled;
|
|
sac.max_cyclomatic_complexity = 0;
|
|
assertAnalyzerWarnings(q{
|
|
unittest // [warn]: Cyclomatic complexity of this function is 1.
|
|
{
|
|
}
|
|
|
|
unittest // [warn]: Cyclomatic complexity of this function is 1.
|
|
{
|
|
writeln("hello");
|
|
writeln("world");
|
|
}
|
|
|
|
void main(string[] args) // [warn]: Cyclomatic complexity of this function is 3.
|
|
{
|
|
if (!args.length)
|
|
return;
|
|
writeln("hello ", args);
|
|
}
|
|
|
|
unittest // [warn]: Cyclomatic complexity of this function is 1.
|
|
{
|
|
// static if / static foreach does not increase cyclomatic complexity
|
|
static if (stuff)
|
|
int a;
|
|
int a;
|
|
}
|
|
|
|
unittest // [warn]: Cyclomatic complexity of this function is 2.
|
|
{
|
|
foreach (i; 0 .. 2)
|
|
{
|
|
}
|
|
int a;
|
|
}
|
|
|
|
unittest // [warn]: Cyclomatic complexity of this function is 3.
|
|
{
|
|
foreach (i; 0 .. 2)
|
|
{
|
|
break;
|
|
}
|
|
int a;
|
|
}
|
|
|
|
unittest // [warn]: Cyclomatic complexity of this function is 2.
|
|
{
|
|
switch (x)
|
|
{
|
|
case 1:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
int a;
|
|
}
|
|
|
|
bool shouldRun(check : BaseAnalyzer)( // [warn]: Cyclomatic complexity of this function is 20.
|
|
string moduleName, const ref StaticAnalysisConfig config)
|
|
{
|
|
enum string a = check.name;
|
|
|
|
if (mixin("config." ~ a) == Check.disabled)
|
|
return false;
|
|
|
|
// By default, run the check
|
|
if (!moduleName.length)
|
|
return true;
|
|
|
|
auto filters = mixin("config.filters." ~ a);
|
|
|
|
// Check if there are filters are defined
|
|
// filters starting with a comma are invalid
|
|
if (filters.length == 0 || filters[0].length == 0)
|
|
return true;
|
|
|
|
auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]);
|
|
auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]);
|
|
|
|
// exclusion has preference over inclusion
|
|
if (!excluders.empty && excluders.any!(s => moduleName.canFind(s)))
|
|
return false;
|
|
|
|
if (!includers.empty)
|
|
return includers.any!(s => moduleName.canFind(s));
|
|
|
|
// by default: include all modules
|
|
return true;
|
|
}
|
|
|
|
}c, sac);
|
|
stderr.writeln("Unittest for CyclomaticComplexityCheck passed.");
|
|
}
|