Use DMD in CyclomaticComplexityCheck (#85)

* Use DMD in CyclomaticComplexityCheck

* Simplify templated visit

* Keep old unit tests
This commit is contained in:
Vladiwostok 2024-03-19 09:51:24 +02:00 committed by GitHub
parent a3c473650b
commit 18b691370a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 277 additions and 101 deletions

View File

@ -4,12 +4,8 @@
module dscanner.analysis.cyclomatic_complexity; module dscanner.analysis.cyclomatic_complexity;
import dparse.ast;
import dparse.lexer;
import dsymbol.scope_ : Scope;
import dscanner.analysis.base; import dscanner.analysis.base;
import dscanner.analysis.helpers; import dmd.location : Loc;
import std.format; import std.format;
/// Implements a basic cyclomatic complexity algorithm using the AST. /// Implements a basic cyclomatic complexity algorithm using the AST.
@ -35,12 +31,9 @@ import std.format;
/// See: https://en.wikipedia.org/wiki/Cyclomatic_complexity /// See: https://en.wikipedia.org/wiki/Cyclomatic_complexity
/// Rules based on http://cyvis.sourceforge.net/cyclomatic_complexity.html /// Rules based on http://cyvis.sourceforge.net/cyclomatic_complexity.html
/// and https://github.com/fzipp/gocyclo /// and https://github.com/fzipp/gocyclo
final class CyclomaticComplexityCheck : BaseAnalyzer extern (C++) class CyclomaticComplexityCheck(AST) : BaseAnalyzerDmd
{ {
/// Message key emitted when the threshold is reached alias visit = BaseAnalyzerDmd.visit;
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"; mixin AnalyzerInfo!"cyclomatic_complexity";
/// Maximum cyclomatic complexity. Once the cyclomatic complexity is greater /// Maximum cyclomatic complexity. Once the cyclomatic complexity is greater
@ -50,52 +43,44 @@ final class CyclomaticComplexityCheck : BaseAnalyzer
/// unmaintainable / untestable. /// unmaintainable / untestable.
/// ///
/// For clean development a threshold like 20 can be used instead. /// For clean development a threshold like 20 can be used instead.
int maxCyclomaticComplexity; immutable int maxCyclomaticComplexity;
/// private enum string KEY = "dscanner.metric.cyclomatic_complexity";
this(BaseAnalyzerArguments args, int maxCyclomaticComplexity = 50) private enum string MESSAGE = "Cyclomatic complexity of this function is %s.";
private int[] complexityStack = [0];
private bool[] inLoop = [false];
extern (D) this(string fileName, bool skipTests = false, int maxCyclomaticComplexity = 50)
{ {
super(args); super(fileName, skipTests);
this.maxCyclomaticComplexity = maxCyclomaticComplexity; this.maxCyclomaticComplexity = maxCyclomaticComplexity;
} }
mixin VisitComplex!IfStatement; override void visit(AST.TemplateDeclaration templateDecl)
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; foreach (member; *templateDecl.members)
scope (exit) member.accept(this);
inLoop.length--;
n.accept(this);
} }
override void visit(const BreakStatement b) override void visit(AST.FuncDeclaration funDecl)
{ {
if (b.label !is Token.init || inLoop[$ - 1]) if (funDecl.fbody is null)
complexityStack[$ - 1]++;
}
override void visit(const FunctionDeclaration fun)
{
if (!fun.functionBody)
return; return;
analyzeFunctionBody(funDecl.fbody, funDecl.loc);
}
override void visit(AST.UnitTestDeclaration unitTestDecl)
{
if (skipTests)
return;
analyzeFunctionBody(unitTestDecl.fbody, unitTestDecl.loc);
}
private void analyzeFunctionBody(AST.Statement functionBody, Loc location)
{
complexityStack.assumeSafeAppend ~= 1; complexityStack.assumeSafeAppend ~= 1;
inLoop.assumeSafeAppend ~= false; inLoop.assumeSafeAppend ~= false;
scope (exit) scope (exit)
@ -103,58 +88,100 @@ final class CyclomaticComplexityCheck : BaseAnalyzer
complexityStack.length--; complexityStack.length--;
inLoop.length--; inLoop.length--;
} }
fun.functionBody.accept(this);
testComplexity(fun.name); functionBody.accept(this);
testComplexity(location.linnum, location.charnum);
} }
override void visit(const Unittest unittest_) private void testComplexity(size_t line, size_t column)
{
if (!skipTests)
{
complexityStack.assumeSafeAppend ~= 1;
inLoop.assumeSafeAppend ~= false;
scope (exit)
{
complexityStack.length--;
inLoop.length--;
}
unittest_.accept(this);
testComplexity(unittest_.findTokenForDisplay(tok!"unittest"));
}
}
alias visit = BaseAnalyzer.visit;
private:
int[] complexityStack = [0];
bool[] inLoop = [false];
void testComplexity(T)(T annotatable)
{ {
auto complexity = complexityStack[$ - 1]; auto complexity = complexityStack[$ - 1];
if (complexity > maxCyclomaticComplexity) if (complexity > maxCyclomaticComplexity)
{ addErrorMessage(line, column, KEY, format!MESSAGE(complexity));
addErrorMessage(annotatable, KEY, format!MESSAGE(complexity));
}
} }
template VisitComplex(NodeType, int increase = 1) override void visit(AST.FuncExp funcExp)
{ {
override void visit(const NodeType n) if (funcExp.fd is null)
return;
complexityStack[$ - 1]++;
funcExp.fd.accept(this);
}
mixin VisitComplex!(AST.IfStatement);
mixin VisitComplex!(AST.LogicalExp);
mixin VisitComplex!(AST.CondExp);
mixin VisitComplex!(AST.CaseStatement);
mixin VisitComplex!(AST.CaseRangeStatement);
mixin VisitComplex!(AST.ReturnStatement);
mixin VisitComplex!(AST.ContinueStatement);
mixin VisitComplex!(AST.GotoStatement);
mixin VisitComplex!(AST.TryFinallyStatement);
mixin VisitComplex!(AST.ThrowExp);
private template VisitComplex(NodeType, int increase = 1)
{
override void visit(NodeType nodeType)
{ {
complexityStack[$ - 1] += increase; complexityStack[$ - 1] += increase;
n.accept(this); super.visit(nodeType);
} }
} }
template VisitLoop(NodeType, int increase = 1) override void visit(AST.SwitchStatement switchStatement)
{ {
override void visit(const NodeType n) inLoop.assumeSafeAppend ~= false;
scope (exit)
inLoop.length--;
switchStatement.condition.accept(this);
switchStatement._body.accept(this);
}
override void visit(AST.BreakStatement breakStatement)
{
if (inLoop[$ - 1])
complexityStack[$ - 1]++;
}
override void visit(AST.TryCatchStatement tryCatchStatement)
{
tryCatchStatement._body.accept(this);
if (tryCatchStatement.catches !is null)
{
foreach (catchStatement; *(tryCatchStatement.catches))
{
complexityStack[$ - 1]++;
catchStatement.handler.accept(this);
}
}
}
override void visit(AST.StaticForeachStatement staticForeachStatement)
{
// StaticForeachStatement visit has to be overridden in order to avoid visiting
// its forEachStatement member, which would increase the complexity.
return;
}
mixin VisitLoop!(AST.DoStatement);
mixin VisitLoop!(AST.WhileStatement);
mixin VisitLoop!(AST.ForStatement);
mixin VisitLoop!(AST.ForeachRangeStatement);
mixin VisitLoop!(AST.ForeachStatement);
private template VisitLoop(NodeType, int increase = 1)
{
override void visit(NodeType nodeType)
{ {
inLoop.assumeSafeAppend ~= true; inLoop.assumeSafeAppend ~= true;
scope (exit) scope (exit)
inLoop.length--; inLoop.length--;
complexityStack[$ - 1] += increase; complexityStack[$ - 1] += increase;
n.accept(this); super.visit(nodeType);
} }
} }
} }
@ -162,34 +189,34 @@ private:
unittest unittest
{ {
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD;
import std.stdio : stderr; import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig(); StaticAnalysisConfig sac = disabledConfig();
sac.cyclomatic_complexity = Check.enabled; sac.cyclomatic_complexity = Check.enabled;
sac.max_cyclomatic_complexity = 0; sac.max_cyclomatic_complexity = 0;
assertAnalyzerWarnings(q{
unittest /+ // TODO: Remove redundant tests and break down remaining tests in individual assertions
^^^^^^^^ [warn]: Cyclomatic complexity of this function is 1. +/ assertAnalyzerWarningsDMD(q{
unittest // [warn]: Cyclomatic complexity of this function is 1.
{ {
} }
unittest /+ // unit test
^^^^^^^^ [warn]: Cyclomatic complexity of this function is 1. +/ unittest // [warn]: Cyclomatic complexity of this function is 1.
{ {
writeln("hello"); writeln("hello");
writeln("world"); writeln("world");
} }
void main(string[] args) /+ void main(string[] args) // [warn]: Cyclomatic complexity of this function is 3.
^^^^ [warn]: Cyclomatic complexity of this function is 3. +/
{ {
if (!args.length) if (!args.length)
return; return;
writeln("hello ", args); writeln("hello ", args);
} }
unittest /+ unittest // [warn]: Cyclomatic complexity of this function is 1.
^^^^^^^^ [warn]: Cyclomatic complexity of this function is 1. +/
{ {
// static if / static foreach does not increase cyclomatic complexity // static if / static foreach does not increase cyclomatic complexity
static if (stuff) static if (stuff)
@ -197,8 +224,7 @@ unittest /+
int a; int a;
} }
unittest /+ unittest // [warn]: Cyclomatic complexity of this function is 2.
^^^^^^^^ [warn]: Cyclomatic complexity of this function is 2. +/
{ {
foreach (i; 0 .. 2) foreach (i; 0 .. 2)
{ {
@ -206,8 +232,7 @@ unittest /+
int a; int a;
} }
unittest /+ unittest // [warn]: Cyclomatic complexity of this function is 3.
^^^^^^^^ [warn]: Cyclomatic complexity of this function is 3. +/
{ {
foreach (i; 0 .. 2) foreach (i; 0 .. 2)
{ {
@ -216,8 +241,7 @@ unittest /+
int a; int a;
} }
unittest /+ unittest // [warn]: Cyclomatic complexity of this function is 2.
^^^^^^^^ [warn]: Cyclomatic complexity of this function is 2. +/
{ {
switch (x) switch (x)
{ {
@ -229,8 +253,8 @@ unittest /+
int a; int a;
} }
bool shouldRun(check : BaseAnalyzer)( /+ // Template, other (tested) stuff
^^^^^^^^^ [warn]: Cyclomatic complexity of this function is 20. +/ bool shouldRun(check : BaseAnalyzer)( // [warn]: Cyclomatic complexity of this function is 20.
string moduleName, const ref StaticAnalysisConfig config) string moduleName, const ref StaticAnalysisConfig config)
{ {
enum string a = check.name; enum string a = check.name;
@ -262,7 +286,157 @@ bool shouldRun(check : BaseAnalyzer)( /+
// by default: include all modules // by default: include all modules
return true; return true;
} }
}c, sac); }c, sac);
assertAnalyzerWarningsDMD(q{
// goto, return
void returnGoto() // [warn]: Cyclomatic complexity of this function is 3.
{
goto hello;
int a = 0;
a += 9;
hello:
return;
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// if, else, ternary operator
void ifElseTernary() // [warn]: Cyclomatic complexity of this function is 4.
{
if (1 > 2)
{
int a;
}
else if (2 > 1)
{
int b;
}
else
{
int c;
}
int d = true ? 1 : 2;
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// static if and static foreach don't increase cyclomatic complexity
void staticIfFor() // [warn]: Cyclomatic complexity of this function is 1.
{
static if (stuff)
int a;
int b;
static foreach(i; 0 .. 10)
{
pragma(msg, i);
}
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// function literal (lambda)
void lambda() // [warn]: Cyclomatic complexity of this function is 2.
{
auto x = (int a) => a + 1;
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// loops: for, foreach, while, do - while
void controlFlow() // [warn]: Cyclomatic complexity of this function is 7.
{
int x = 0;
for (int i = 0; i < 100; i++)
{
i++;
}
foreach (i; 0 .. 2)
{
x += i;
continue;
}
while (true)
{
break;
}
do
{
int x = 0;
} while (true);
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// switch - case
void switchCaseCaseRange() // [warn]: Cyclomatic complexity of this function is 5.
{
switch (x)
{
case 1:
break;
case 2:
case 3:
break;
case 7: .. case 10:
break;
default:
break;
}
int a;
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// if, else, logical expressions
void ifConditions() // [warn]: Cyclomatic complexity of this function is 5.
{
if (true && false)
{
doX();
}
else if (true || false)
{
doY();
}
}
}c, sac);
assertAnalyzerWarningsDMD(q{
// catch, throw
void throwCatch() // [warn]: Cyclomatic complexity of this function is 5.
{
int x;
try
{
x = 5;
}
catch (Exception e)
{
x = 7;
}
catch (Exception a)
{
x = 8;
}
catch (Exception x)
{
throw new Exception("Exception");
}
finally
{
x = 9;
}
}
}c, sac);
stderr.writeln("Unittest for CyclomaticComplexityCheck passed."); stderr.writeln("Unittest for CyclomaticComplexityCheck passed.");
} }

View File

@ -908,11 +908,6 @@ private BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
checks ~= new UnusedResultChecker(args.setSkipTests( checks ~= new UnusedResultChecker(args.setSkipTests(
analysisConfig.unused_result == Check.skipTests && !ut)); analysisConfig.unused_result == Check.skipTests && !ut));
if (moduleName.shouldRun!CyclomaticComplexityCheck(analysisConfig))
checks ~= new CyclomaticComplexityCheck(args.setSkipTests(
analysisConfig.cyclomatic_complexity == Check.skipTests && !ut),
analysisConfig.max_cyclomatic_complexity.to!int);
if (moduleName.shouldRun!BodyOnDisabledFuncsCheck(analysisConfig)) if (moduleName.shouldRun!BodyOnDisabledFuncsCheck(analysisConfig))
checks ~= new BodyOnDisabledFuncsCheck(args.setSkipTests( checks ~= new BodyOnDisabledFuncsCheck(args.setSkipTests(
analysisConfig.body_on_disabled_func_check == Check.skipTests && !ut)); analysisConfig.body_on_disabled_func_check == Check.skipTests && !ut));
@ -1349,6 +1344,13 @@ MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleN
config.if_else_same_check == Check.skipTests && !ut config.if_else_same_check == Check.skipTests && !ut
); );
if (moduleName.shouldRunDmd!(CyclomaticComplexityCheck!ASTCodegen)(config))
visitors ~= new CyclomaticComplexityCheck!ASTCodegen(
fileName,
config.cyclomatic_complexity == Check.skipTests && !ut,
config.max_cyclomatic_complexity.to!int
);
foreach (visitor; visitors) foreach (visitor; visitors)
{ {
m.accept(visitor); m.accept(visitor);