CodeMetric
Investigating C++ cyclometric complexity

AnalyseGui.h
#ifndef _CppAnalyse_AnalyseGui_h
#define _CppAnalyse_AnalyseGui_h
#include <CtrlLib/CtrlLib.h>
using namespace Upp;
class WarningDisplay : public Display
{
int limit;
Color warningColor;
public:
WarningDisplay(int limit, Color warningColor = Color(255, 128, 128));
void PaintBackground(Draw& w, const Rect& r, const Value& q,
Color ink, Color paper, dword style) const;
};
class AnalyseGui : public TopWindow
{
public:
typedef AnalyseGui CLASSNAME;
MenuBar menu;
Splitter splitter;
LineEdit source;
ArrayCtrl chart;
LineEdit textMetric;
WarningDisplay ccDisplay, llocDisplay, depthDisplay;
AnalyseGui();
void MainMenu(Bar& menu);
void Open();
void UpdateMetric();
void GotoFunction();
};
#endif
Analyse.h
#ifndef _CppAnalyse_Analyze_h_
#define _CppAnalyse_Analyze_h_
#include <CppBase/CppBase.h>
using namespace Upp;
struct CodeMetric
{
public:
struct FunctionEntry : public Moveable<FunctionEntry>
{
String name;
int pos;
int cyclomaticComplexity1;
int cyclomaticComplexity2;
int logicalLinesOfCode;
int scopeDepth;
};
int orphanLines, blankLines, commentLines;
int totalLLOC, sumCC1, sumCC2, sumDepth;
Vector<FunctionEntry> functions;
String errors;
explicit CodeMetric(const String &fileContent);
String ToString() const;
private:
typedef CodeMetric CLASSNAME;
void StoreError(int line, const String &msg);
void StoreMetric(const Parser::FunctionStat & functionStat);
int LogicalLinesOfCode(const LexSymbolStat &symbolStat);
};
#endif
main.cpp
#include "AnalyseGui.h"
#include "Analyse.h"
#define IMAGECLASS Images
#define IMAGEFILE <CodeMetric/AnalyseGui.iml>
#include <Draw/iml.h>
String defaultCode =
"/*"
" This tool measures certain source code metrics.\n"
" You can type, paste, or load the code you would like measure into this text area.\n"
" Metrics provided are:\n"
" - Cyclomatic complexity 1: counts the decision points per method, thus estimating the\n"
" number of testcases needed for each method.\n"
" - Cyclomatic complexity 2: CC1 extended with the implicit decisions created by the\n"
" && || and ?: operators. Evaluation:\n"
" CC2 1-10 simple, low risk method\n"
" CC2 10-20 moderate complexity & risk method\n"
" CC2 21-50 high complexity & risk method\n"
" CC2 >50 too complex, untestable method, should be refactored\n"
" - Depth: measures deepest scope embedding level per method. This estimates the human\n"
" memory needed to keep in mind the current context. Any methods with depth > 5 is\n"
" considered too complex, and candidate for refactoring\n"
" - Logical Lines Of Code: estimates the amount of source code per method in a way\n"
" which is mostly independent from code formatting style. Methods longer than 80 LLOC are\n"
" too long, and should be refactored.\n"
"*/\n"
"int main()\n"
"{\n"
" return 0;\n"
"}\n";
String defaultTitle = "CodeMetric GUI";
WarningDisplay::WarningDisplay(int limit, Color warningColor) :
limit(limit), warningColor(warningColor)
{
}
void WarningDisplay::PaintBackground(Draw& w, const Rect& r, const Value& q,
Color ink, Color paper, dword style) const
{
int v = (int)q;
if(v >= limit)
paper = warningColor;
Display::PaintBackground(w, r, q, ink, paper, style);
}
void AnalyseGui::MainMenu(Bar &bar)
{
bar.Add("Load file", THISBACK(Open));
}
void AnalyseGui::Open()
{
FileSelector fsel;
fsel.ExecuteOpen("Select file");
String fileName = fsel.Get();
if(fileName == "")
return;
source <<= LoadFile(fileName);
UpdateMetric();
Title(defaultTitle + " [" + fileName + "]");
}
void AnalyseGui::UpdateMetric()
{
String s = ~source;
CodeMetric metric(s);
chart.Clear();
for(int i = 0; i < metric.functions.GetCount(); i++)
{
CodeMetric::FunctionEntry & entry =
metric.functions[i];
chart.Add(entry.name,
entry.pos,
entry.cyclomaticComplexity1,
entry.cyclomaticComplexity2,
entry.logicalLinesOfCode,
entry.scopeDepth);
}
textMetric.SetData(metric.ToString());
}
void AnalyseGui::GotoFunction()
{
int cursor = chart.GetCursor();
if(cursor >= 0 && cursor < chart.GetCount())
{
int pos = (int)chart.Get(cursor, 1);
source.SetCursor( source.GetPos(pos) );
source.CenterCursor();
}
}
AnalyseGui::AnalyseGui() :
ccDisplay(50), llocDisplay(80), depthDisplay(5)
{
Title(defaultTitle);
Icon(Images::GaugeIcon);
AddFrame(menu);
menu.Set( THISBACK(MainMenu) );
source <<= THISBACK(UpdateMetric);
source <<= defaultCode;
textMetric.SetEditable(false);
chart.AddColumn("Function", 80);
chart.AddColumn("Pos", 15);
chart.AddColumn("CC1", 15).SetDisplay(ccDisplay);
chart.AddColumn("CC2", 15).SetDisplay(ccDisplay);
chart.AddColumn("LLOC", 15).SetDisplay(llocDisplay);
chart.AddColumn("Depth", 15).SetDisplay(depthDisplay);
chart.WhenLeftDouble = THISBACK(GotoFunction);
UpdateMetric();
splitter.Vert() << source << chart << textMetric;
splitter.SetPos(9000, 1);
splitter.SetPos(6500, 0);
Add(splitter.SizePos());
Sizeable().Zoomable();
}
GUI_APP_MAIN
{
AnalyseGui().Run();
}
Analyse.cpp
#include <CppBase/CppBase.h>
#include "Analyse.h"
bool ContainsAt(const String &source, const String &pattern, int pos = 0)
{
return pos >= 0
&& pos + pattern.GetLength() <= source.GetLength()
&& 0 == memcmp(source.Begin() + pos, pattern.Begin(), pattern.GetLength());
}
bool StartsWith(const String &source, const String &pattern)
{
return ContainsAt(source, pattern, 0);
}
bool EndsWith(const String &source, const String &pattern)
{
return ContainsAt(source, pattern, source.GetLength() - pattern.GetLength());
}
String InsertNestingToSignature(String natural, String nesting)
{
if(StartsWith(nesting, "::"))
nesting.Remove(0, 2);
if(nesting.GetCount() && !EndsWith(nesting, "::"))
nesting << "::";
int pos = natural.Find('('); // find the first opening parenthesis
pos--;
while(pos >= 0 && !iscid(natural[pos])) // skip over non-id chars before paren.
pos--;
if(pos < 0) return "";
while(pos >= 0 && iscid(natural[pos])) // skip over last id before paren
pos--;
natural.Insert(pos+1, nesting);
return natural;
}
CodeMetric::CodeMetric(const String &fileContent) :
orphanLines(0), blankLines(0), commentLines(0)
{
StringStream stream(fileContent);
CppBase base;
Parser parser;
parser.whenFnEnd = THISBACK(StoreMetric);
parser.Do(stream, base, 0, 0,
"file", CNULL,
Vector<String>(),
Vector<String>(),
Index<String>());
const SrcFile &srcFile = parser.getPreprocessedFile();
commentLines = srcFile.commentLinesRemoved;
blankLines = srcFile.blankLinesRemoved;
orphanLines = parser.symbolsOutsideFunctions.GetStat(';');
totalLLOC = orphanLines;
sumCC1 = sumCC2 = sumDepth = 0;
for(int i = 0; i < functions.GetCount(); i++) {
totalLLOC += functions[i].logicalLinesOfCode;
sumCC1 += functions[i].cyclomaticComplexity1;
sumCC2 += functions[i].cyclomaticComplexity2;
sumDepth += functions[i].scopeDepth;
}
}
String CodeMetric::ToString() const
{
String s;
s << "LLOC: " << totalLLOC
<< ", Blank: " << blankLines
<< ", Comments: " << commentLines;
if(errors != "")
s << "\nErrors:\n" << errors;
return s;
}
void CodeMetric::StoreError(int line, const String &msg)
{
errors << "line " << line << ": " << msg << "\n";
}
int CodeMetric::LogicalLinesOfCode(const LexSymbolStat &symbolStat)
{
static Vector<int> oneLiners(pick(
Vector<int>() << tk_if << tk_else << tk_switch << tk_case
<< tk_for << tk_do << tk_while << tk_try << tk_catch
<< tk_struct << tk_class << tk_namespace
<< tk_public << tk_private << tk_protected
<< ';'));
return symbolStat.SumStat( oneLiners );
}
void CodeMetric::StoreMetric(const Parser::FunctionStat & functionStat)
{
static Vector<int> cc1_symbols(pick(
Vector<int>() << tk_if << tk_case << tk_for << tk_while << tk_catch));
static Vector<int> cc2_symbols(pick(
Vector<int>() << t_and << t_or << '?'));
if(!functionStat.cppItem.impl)
return;
FunctionEntry &entry = functions.Add();
entry.pos = functionStat.cppItem.line;
entry.name = InsertNestingToSignature(functionStat.cppItem.natural,
functionStat.scope);
int cc1 = 1 + functionStat.symbolStat.SumStat( cc1_symbols );
entry.cyclomaticComplexity1 = cc1;
entry.cyclomaticComplexity2 = cc1 + functionStat.symbolStat.SumStat( cc2_symbols );
entry.logicalLinesOfCode = 2 + LogicalLinesOfCode(functionStat.symbolStat);
entry.scopeDepth = functionStat.maxScopeDepth;
}
|