D-Scanner/src/dscanner/analysis/cyclomatic_complexity.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.");
}