mirror of https://gitlab.com/basile.b/dexed.git
#104, start Halstead metrics
This commit is contained in:
parent
e91831c3b7
commit
af7d5d6c34
|
@ -41,6 +41,7 @@ object CurrentProject: TCENativeProject
|
|||
'src/imports.d'
|
||||
'src/mainfun.d'
|
||||
'src/common.d'
|
||||
'src/halstead.d'
|
||||
)
|
||||
ConfigurationIndex = 1
|
||||
end
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import
|
|||
import
|
||||
dparse.lexer, dparse.parser, dparse.ast, dparse.rollback_allocator;
|
||||
import
|
||||
common, todos, symlist, imports, mainfun;
|
||||
common, todos, symlist, imports, mainfun, halstead;
|
||||
|
||||
|
||||
private __gshared bool deepSymList;
|
||||
|
@ -59,6 +59,7 @@ void main(string[] args)
|
|||
"m", &handleMainfunOption,
|
||||
"s", &handleSymListOption,
|
||||
"t", &handleTodosOption,
|
||||
"H", &handleHalsteadOption,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -120,6 +121,21 @@ void handleMainfunOption()
|
|||
.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,
|
||||
bool err)
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@ unit ce_dastworx;
|
|||
interface
|
||||
|
||||
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
|
||||
|
@ -20,6 +20,8 @@ procedure getModuleImports(source, imports: TStrings);
|
|||
*)
|
||||
procedure getModulesImports(const files: string; results: TStrings);
|
||||
|
||||
procedure getHalsteadMetrics(source: TStrings; out jsn: TJSONObject);
|
||||
|
||||
implementation
|
||||
|
||||
var
|
||||
|
@ -85,5 +87,39 @@ begin
|
|||
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.
|
||||
|
||||
|
|
|
@ -2387,6 +2387,9 @@ object CEMainForm: TCEMainForm
|
|||
AC002178AD002178AD002178AD002178AD002178AD002178AD00
|
||||
}
|
||||
end
|
||||
object MenuItem77: TMenuItem
|
||||
Action = actFileMetricsHalstead
|
||||
end
|
||||
object MenuItem60: TMenuItem
|
||||
Action = actFileOpenContFold
|
||||
Bitmap.Data = {
|
||||
|
@ -5190,6 +5193,12 @@ object CEMainForm: TCEMainForm
|
|||
ImageIndex = 27
|
||||
OnExecute = actProjNewGroupExecute
|
||||
end
|
||||
object actFileMetricsHalstead: TAction
|
||||
Category = 'File'
|
||||
Caption = 'View Halstead metrics'
|
||||
ImageIndex = 35
|
||||
OnExecute = actFileMetricsHalsteadExecute
|
||||
end
|
||||
end
|
||||
object imgList: TImageList
|
||||
left = 64
|
||||
|
|
|
@ -7,7 +7,7 @@ interface
|
|||
uses
|
||||
Classes, SysUtils, LazFileUtils, SynEditKeyCmds, SynHighlighterLFM, Forms,
|
||||
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,
|
||||
xfpjson, xjsonparser, xjsonscanner,
|
||||
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_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_dfmt, ce_lcldragdrop, ce_projgroup, ce_projutils, ce_stringrange;
|
||||
ce_dfmt, ce_lcldragdrop, ce_projgroup, ce_projutils, ce_stringrange, ce_dastworx;
|
||||
|
||||
type
|
||||
|
||||
|
@ -101,6 +101,7 @@ type
|
|||
actFileRunDub: TAction;
|
||||
actFileRunDubOut: TAction;
|
||||
actFileNewDubScript: TAction;
|
||||
actFileMetricsHalstead: TAction;
|
||||
actProjGroupCompileCustomSync: TAction;
|
||||
actProjGroupClose: TAction;
|
||||
actProjGroupCompileSync: TAction;
|
||||
|
@ -149,6 +150,7 @@ type
|
|||
MenuItem103: TMenuItem;
|
||||
MenuItem104: TMenuItem;
|
||||
MenuItem105: TMenuItem;
|
||||
MenuItem77: TMenuItem;
|
||||
mnuOpts: TMenuItem;
|
||||
mnuItemMruGroup: TMenuItem;
|
||||
MenuItem11: TMenuItem;
|
||||
|
@ -253,6 +255,7 @@ type
|
|||
MenuItem9: TMenuItem;
|
||||
procedure actFileCompileExecute(Sender: TObject);
|
||||
procedure actFileDscannerExecute(Sender: TObject);
|
||||
procedure actFileMetricsHalsteadExecute(Sender: TObject);
|
||||
procedure actFileNewDubScriptExecute(Sender: TObject);
|
||||
procedure actFileRunDubExecute(Sender: TObject);
|
||||
procedure actFileRunDubOutExecute(Sender: TObject);
|
||||
|
@ -2872,6 +2875,84 @@ begin
|
|||
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);
|
||||
begin
|
||||
newFile;
|
||||
|
|
Loading…
Reference in New Issue