#104, start Halstead metrics

This commit is contained in:
Basile Burg 2016-11-14 15:05:34 +01:00
parent e91831c3b7
commit af7d5d6c34
No known key found for this signature in database
GPG Key ID: 1868039F415CB8CF
6 changed files with 536 additions and 4 deletions

View File

@ -41,6 +41,7 @@ object CurrentProject: TCENativeProject
'src/imports.d' 'src/imports.d'
'src/mainfun.d' 'src/mainfun.d'
'src/common.d' 'src/common.d'
'src/halstead.d'
) )
ConfigurationIndex = 1 ConfigurationIndex = 1
end end

389
dastworx/src/halstead.d Normal file
View File

@ -0,0 +1,389 @@
module halstead;
import
std.meta, std.traits, std.algorithm.iteration, std.json;
import
dparse.lexer, dparse.parser, dparse.ast, dparse.rollback_allocator;
import
iz.memory, iz.containers;
version(unittest){} else import
common;
void performHalsteadMetrics(const(Module) mod)
{
HalsteadMetric hm = construct!(HalsteadMetric);
hm.visit(mod);
hm.serialize;
}
private struct Function
{
size_t line;
string name;
size_t N1, n1;
size_t N2, n2;
}
private final class HalsteadMetric: ASTVisitor
{
alias visit = ASTVisitor.visit;
Function[] functions;
size_t[string] operators;
size_t[string] operands;
size_t functionNesting;
bool functionCall;
bool ifStatement;
JSONValue fs;
this()
{
fs = parseJSON("[]");
}
void serialize()
{
import std.stdio: write;
JSONValue js;
js["functions"] = fs;
js.toString.write;
}
override void visit(const(PragmaExpression)){}
//TODO: add share/static/__ctor & __dtor
override void visit(const(FunctionDeclaration) decl)
{
if (!decl.functionBody)
return;
if (functionNesting++ == 0)
functions.length = functions.length + 1;
decl.accept(this);
functions[$-1].name = decl.name.text;
functions[$-1].line = decl.name.line;
if (operators.length)
{
functions[$-1].N1 = operators.byValue.fold!((a,b) => b = a + b);
functions[$-1].n1 = operators.length;
}
if (operands.length)
{
functions[$-1].N2 = operands.byValue.fold!((a,b) => b = a + b);
functions[$-1].n2 = operands.length;
}
JSONValue f;
f["name"] = functions[$-1].name;
f["line"] = functions[$-1].line;
f["n1Sum"] = functions[$-1].N1;
f["n1Count"] = functions[$-1].n1;
f["n2Sum"] = functions[$-1].N2;
f["n2Count"] = functions[$-1].n2;
fs ~= [f];
version(unittest)
{
import std.stdio;
writeln(functions[$-1]);
writeln('\t',operators);
writeln('\t',operands);
}
operators.clear;
operands.clear;
functionNesting--;
}
override void visit(const(PrimaryExpression) primary)
{
if (primary.identifierOrTemplateInstance !is null
&& primary.identifierOrTemplateInstance.identifier != tok!"")
{
if (!functionCall)
++operands[primary.identifierOrTemplateInstance.identifier.text];
else
++operators[primary.identifierOrTemplateInstance.identifier.text];
}
else if (primary.primary.type.isLiteral)
{
import std.digest.crc: crc32Of, toHexString;
++operands["literal" ~ primary.primary.text.crc32Of.toHexString.idup];
}
functionCall = false;
primary.accept(this);
}
override void visit(const(UnaryExpression) expr)
{
if (expr.prefix.type)
++operators[str(expr.prefix.type)];
if (expr.suffix.type)
++operators[str(expr.suffix.type)];
// TODO: detect function name here
if (expr.functionCallExpression)
functionCall = true;
// TODO: detect function call w/o parens
//else if (expr.prefix.type == tok!"" && expr.suffix.type == tok!"")
// functionCall = true;
expr.accept(this);
}
override void visit(const(AndAndExpression) expr)
{
++operators["&&"];
expr.accept(this);
}
override void visit(const(OrOrExpression) expr)
{
++operators["||"];
expr.accept(this);
}
override void visit(const(AndExpression) expr)
{
++operators["&"];
expr.accept(this);
}
override void visit(const(AsmAndExp) expr)
{
++operators["&"];
expr.accept(this);
}
override void visit(const(OrExpression) expr)
{
++operators["|"];
expr.accept(this);
}
override void visit(const(InExpression) expr)
{
++operators["in"];
expr.accept(this);
}
override void visit(const(PowExpression) expr)
{
++operators["^"];
expr.accept(this);
}
override void visit(const(XorExpression) expr)
{
++operators["^^"];
expr.accept(this);
}
override void visit(const(IndexExpression) expr)
{
++operators["[]"];
expr.accept(this);
}
override void visit(const(NewExpression) expr)
{
++operators["new"];
expr.accept(this);
}
override void visit(const(NewAnonClassExpression) expr)
{
++operators["new"];
expr.accept(this);
}
override void visit(const(CastExpression) expr)
{
++operators["cast"];
expr.accept(this);
}
override void visit(const(IsExpression) expr)
{
++operators["is"];
expr.accept(this);
}
override void visit(const(TypeidExpression) expr)
{
++operators["typeid"];
expr.accept(this);
}
override void visit(const(IfStatement) st)
{
++operators["if"];
ifStatement = true;
st.accept(this);
ifStatement = false;
}
override void visit(const(DeclarationOrStatement) st)
{
if (ifStatement && st.statement)
++operators["thenOrElse"];
st.accept(this);
}
override void visit(const(WhileStatement) st)
{
++operators["while"];
st.accept(this);
}
override void visit(const(ForStatement) st)
{
++operators["for"];
st.accept(this);
}
override void visit(const(ForeachStatement) st)
{
++operators["foreach"];
st.accept(this);
}
override void visit(const(ReturnStatement) st)
{
++operators["return"];
st.accept(this);
}
override void visit(const(BreakStatement) st)
{
++operators["break"];
st.accept(this);
}
override void visit(const(ContinueStatement) st)
{
++operators["continue"];
st.accept(this);
}
override void visit(const(GotoStatement) st)
{
++operators["goto"];
st.accept(this);
}
override void visit(const(SwitchStatement) st)
{
++operators["switch"];
st.accept(this);
}
override void visit(const(CaseStatement) st)
{
++operators["case"];
st.accept(this);
}
override void visit(const(CaseRangeStatement) st)
{
++operators["case"];
st.accept(this);
}
override void visit(const(DefaultStatement) st)
{
++operators["case"];
st.accept(this);
}
override void visit(const(ThrowStatement) st)
{
++operators["throw"];
st.accept(this);
}
override void visit(const(TryStatement) st)
{
++operators["try"];
st.accept(this);
}
static string exprAliases()
{
import std.range: iota;
alias ExprWithOp = AliasSeq!(
AddExpression,
AsmAddExp,
AsmEqualExp,
AsmMulExp,
AsmRelExp,
AsmShiftExp,
AssignExpression,
EqualExpression,
MulExpression,
RelExpression,
ShiftExpression,
);
enum exprOverride(T) = "
override void visit(const(" ~ T.stringof ~ ") expr)
{
static assert(__traits(hasMember," ~ T.stringof ~ ", \"operator\"));
++operators[str(expr.operator)];
expr.accept(this);
}";
string result;
foreach(i; aliasSeqOf!(iota(0, ExprWithOp.length)))
result ~= exprOverride!(ExprWithOp[i]);
return result;
}
mixin(exprAliases);
}
version(unittest)
{
T parseAndVisit(T : ASTVisitor)(const(char)[] source)
{
RollbackAllocator allocator;
LexerConfig config = LexerConfig("", StringBehavior.source, WhitespaceBehavior.skip);
StringCache cache = StringCache(StringCache.defaultBucketCount);
const(Token)[] tokens = getTokensForParser(cast(ubyte[]) source, config, &cache);
Module mod = parseModule(tokens, "", &allocator);
T result = construct!(T);
result.visit(mod);
return result;
}
void test(T)(T t)
{
auto a = 1;
auto b = a++;
auto c = a || b;
auto d = a << c;
auto e = a >>> c;
test(test());
test;
}
unittest
{
import std.file;
char[] source = cast(char[]) __FILE__.read;
auto r = source.parseAndVisit!HalsteadMetric;
}
}

View File

@ -9,7 +9,7 @@ import
import import
dparse.lexer, dparse.parser, dparse.ast, dparse.rollback_allocator; dparse.lexer, dparse.parser, dparse.ast, dparse.rollback_allocator;
import import
common, todos, symlist, imports, mainfun; common, todos, symlist, imports, mainfun, halstead;
private __gshared bool deepSymList; private __gshared bool deepSymList;
@ -59,6 +59,7 @@ void main(string[] args)
"m", &handleMainfunOption, "m", &handleMainfunOption,
"s", &handleSymListOption, "s", &handleSymListOption,
"t", &handleTodosOption, "t", &handleTodosOption,
"H", &handleHalsteadOption,
); );
} }
@ -120,6 +121,21 @@ void handleMainfunOption()
.detectMainFun(); .detectMainFun();
} }
/// Handles the "-H" option: write the halstead metrics
void handleHalsteadOption()
{
mixin(logCall);
RollbackAllocator alloc;
StringCache cache = StringCache(StringCache.defaultBucketCount);
LexerConfig config = LexerConfig("", StringBehavior.source);
source.data
.getTokensForParser(config, &cache)
.parseModule("", &alloc, &ignoreErrors)
.performHalsteadMetrics;
}
private void handleErrors(string fname, size_t line, size_t col, string message, private void handleErrors(string fname, size_t line, size_t col, string message,
bool err) bool err)
{ {

View File

@ -4,7 +4,7 @@ unit ce_dastworx;
interface interface
uses uses
Classes, SysUtils, process, ce_common; Classes, SysUtils, process, xjsonscanner, xfpjson, xjsonparser, ce_common;
(** (**
* Gets the module name and the imports of the source code located in * Gets the module name and the imports of the source code located in
@ -20,6 +20,8 @@ procedure getModuleImports(source, imports: TStrings);
*) *)
procedure getModulesImports(const files: string; results: TStrings); procedure getModulesImports(const files: string; results: TStrings);
procedure getHalsteadMetrics(source: TStrings; out jsn: TJSONObject);
implementation implementation
var var
@ -85,5 +87,39 @@ begin
end; end;
end; end;
procedure getHalsteadMetrics(source: TStrings; out jsn: TJSONObject);
var
prc: TProcess;
prs: TJSONParser;
jps: TJSONData;
str: string;
begin
str := getToolName;
if str.isEmpty then
exit;
prc := TProcess.Create(nil);
try
prc.Executable := str;
prc.Parameters.Add('-H');
prc.Options := [poUsePipes {$IFDEF WINDOWS}, poNewConsole{$ENDIF}];
prc.ShowWindow := swoHIDE;
prc.Execute;
str := source.Text;
prc.Input.Write(str[1], str.length);
prc.CloseInput;
prs := TJSONParser.Create(prc.Output, [joIgnoreTrailingComma, joUTF8]);
jps := prs.Parse;
if jps.isNotNil and (jps.JSONType = jtObject) then
jsn := TJSONObject(jps.Clone);
jps.Free;
while prc.Running do ;
// TODO-cmaintenance: remove this from version 3 gold
tryRaiseFromStdErr(prc);
finally
prs.Free;
prc.Free;
end;
end;
end. end.

View File

@ -2387,6 +2387,9 @@ object CEMainForm: TCEMainForm
AC002178AD002178AD002178AD002178AD002178AD002178AD00 AC002178AD002178AD002178AD002178AD002178AD002178AD00
} }
end end
object MenuItem77: TMenuItem
Action = actFileMetricsHalstead
end
object MenuItem60: TMenuItem object MenuItem60: TMenuItem
Action = actFileOpenContFold Action = actFileOpenContFold
Bitmap.Data = { Bitmap.Data = {
@ -5190,6 +5193,12 @@ object CEMainForm: TCEMainForm
ImageIndex = 27 ImageIndex = 27
OnExecute = actProjNewGroupExecute OnExecute = actProjNewGroupExecute
end end
object actFileMetricsHalstead: TAction
Category = 'File'
Caption = 'View Halstead metrics'
ImageIndex = 35
OnExecute = actFileMetricsHalsteadExecute
end
end end
object imgList: TImageList object imgList: TImageList
left = 64 left = 64

View File

@ -7,7 +7,7 @@ interface
uses uses
Classes, SysUtils, LazFileUtils, SynEditKeyCmds, SynHighlighterLFM, Forms, Classes, SysUtils, LazFileUtils, SynEditKeyCmds, SynHighlighterLFM, Forms,
StdCtrls, AnchorDocking, AnchorDockStorage, AnchorDockOptionsDlg, Controls, StdCtrls, AnchorDocking, AnchorDockStorage, AnchorDockOptionsDlg, Controls,
Graphics, strutils, Dialogs, Menus, ActnList, ExtCtrls, process, Graphics, strutils, Dialogs, Menus, ActnList, ExtCtrls, process, math,
{$IFDEF WINDOWS}Windows, {$ENDIF} XMLPropStorage, SynExportHTML, fphttpclient, {$IFDEF WINDOWS}Windows, {$ENDIF} XMLPropStorage, SynExportHTML, fphttpclient,
xfpjson, xjsonparser, xjsonscanner, xfpjson, xjsonparser, xjsonscanner,
ce_common, ce_dmdwrap, ce_ceproject, ce_synmemo, ce_writableComponent, ce_common, ce_dmdwrap, ce_ceproject, ce_synmemo, ce_writableComponent,
@ -15,7 +15,7 @@ uses
ce_search, ce_miniexplorer, ce_libman, ce_libmaneditor, ce_todolist, ce_observer, ce_search, ce_miniexplorer, ce_libman, ce_libmaneditor, ce_todolist, ce_observer,
ce_toolseditor, ce_procinput, ce_optionseditor, ce_symlist, ce_mru, ce_processes, ce_toolseditor, ce_procinput, ce_optionseditor, ce_symlist, ce_mru, ce_processes,
ce_infos, ce_dubproject, ce_dialogs, ce_dubprojeditor,{$IFDEF UNIX} ce_gdb,{$ENDIF} ce_infos, ce_dubproject, ce_dialogs, ce_dubprojeditor,{$IFDEF UNIX} ce_gdb,{$ENDIF}
ce_dfmt, ce_lcldragdrop, ce_projgroup, ce_projutils, ce_stringrange; ce_dfmt, ce_lcldragdrop, ce_projgroup, ce_projutils, ce_stringrange, ce_dastworx;
type type
@ -101,6 +101,7 @@ type
actFileRunDub: TAction; actFileRunDub: TAction;
actFileRunDubOut: TAction; actFileRunDubOut: TAction;
actFileNewDubScript: TAction; actFileNewDubScript: TAction;
actFileMetricsHalstead: TAction;
actProjGroupCompileCustomSync: TAction; actProjGroupCompileCustomSync: TAction;
actProjGroupClose: TAction; actProjGroupClose: TAction;
actProjGroupCompileSync: TAction; actProjGroupCompileSync: TAction;
@ -149,6 +150,7 @@ type
MenuItem103: TMenuItem; MenuItem103: TMenuItem;
MenuItem104: TMenuItem; MenuItem104: TMenuItem;
MenuItem105: TMenuItem; MenuItem105: TMenuItem;
MenuItem77: TMenuItem;
mnuOpts: TMenuItem; mnuOpts: TMenuItem;
mnuItemMruGroup: TMenuItem; mnuItemMruGroup: TMenuItem;
MenuItem11: TMenuItem; MenuItem11: TMenuItem;
@ -253,6 +255,7 @@ type
MenuItem9: TMenuItem; MenuItem9: TMenuItem;
procedure actFileCompileExecute(Sender: TObject); procedure actFileCompileExecute(Sender: TObject);
procedure actFileDscannerExecute(Sender: TObject); procedure actFileDscannerExecute(Sender: TObject);
procedure actFileMetricsHalsteadExecute(Sender: TObject);
procedure actFileNewDubScriptExecute(Sender: TObject); procedure actFileNewDubScriptExecute(Sender: TObject);
procedure actFileRunDubExecute(Sender: TObject); procedure actFileRunDubExecute(Sender: TObject);
procedure actFileRunDubOutExecute(Sender: TObject); procedure actFileRunDubOutExecute(Sender: TObject);
@ -2872,6 +2875,84 @@ begin
end; end;
end; end;
procedure TCEMainForm.actFileMetricsHalsteadExecute(Sender: TObject);
procedure computeMetrics(const obj: TJSONObject);
var
n1, sn1, n2, sn2: integer;
val: TJSONData;
voc, len, line: integer;
vol, dif, eff: single;
begin
val := obj.Find('n1Count');
if val.isNil then
exit;
n1 := val.AsInteger;
val := obj.Find('n1Sum');
if val.isNil then
exit;
sn1 := val.AsInteger;
val := obj.Find('n2Count');
if val.isNil then
exit;
n2 := val.AsInteger;
val := obj.Find('n2Sum');
if val.isNil then
exit;
sn2 := val.AsInteger;
val := obj.Find('line');
if val.isNil then
exit;
line := val.AsInteger;
val := obj.Find('name');
if val.isNil then
exit;
fMsgs.message(format('%s(%d): Halstead metrics for "%s"',
[fDoc.fileName, line, val.AsString]), fDoc, amcEdit, amkInf);
voc := n1 + n2;
len := sn1 + sn2;
vol := len * log2(voc);
dif := n1 * 0.5 * (sn2 / n2);
eff := dif * vol;
fMsgs.message(format(' Vocabulary: %d', [voc]), fDoc, amcEdit, amkInf);
fMsgs.message(format(' Length: %d', [len]), fDoc, amcEdit, amkInf);
fMsgs.message(format(' Volume: %.2f', [vol]), fDoc, amcEdit, amkInf);
fMsgs.message(format(' Difficulty: %.2f', [dif]), fDoc, amcEdit, amkInf);
fMsgs.message(format(' Effort: %.2f', [eff]), fDoc, amcEdit, amkInf);
fMsgs.message(format(' Time required: %.2f secs.', [eff / 18]), fDoc, amcEdit, amkInf);
end;
var
jsn: TJSONObject = nil;
fnc: TJSONObject = nil;
val: TJSONData;
arr: TJSONArray;
i: integer;
begin
if fDoc.isNil then
exit;
getHalsteadMetrics(fDoc.Lines, jsn);
if jsn.isNil then
exit;
val := jsn.Find('functions');
if val.isNil or (val.JSONType <> jtArray) then
exit;
arr := TJSONArray(val);
for i := 0 to arr.Count-1 do
begin
fnc := TJSONObject(arr.Objects[i]);
if fnc.isNotNil then
computeMetrics(fnc);
end;
jsn.Free;
end;
procedure TCEMainForm.actFileNewDubScriptExecute(Sender: TObject); procedure TCEMainForm.actFileNewDubScriptExecute(Sender: TObject);
begin begin
newFile; newFile;