From af7d5d6c344ce354cfefbe9dbae1ad6b3d11aaa4 Mon Sep 17 00:00:00 2001 From: Basile Burg Date: Mon, 14 Nov 2016 15:05:34 +0100 Subject: [PATCH] #104, start Halstead metrics --- dastworx/dastworx.ce | 1 + dastworx/src/halstead.d | 389 ++++++++++++++++++++++++++++++++++++++++ dastworx/src/main.d | 18 +- src/ce_dastworx.pas | 38 +++- src/ce_main.lfm | 9 + src/ce_main.pas | 85 ++++++++- 6 files changed, 536 insertions(+), 4 deletions(-) create mode 100644 dastworx/src/halstead.d diff --git a/dastworx/dastworx.ce b/dastworx/dastworx.ce index 34d633c2..8e761316 100644 --- a/dastworx/dastworx.ce +++ b/dastworx/dastworx.ce @@ -41,6 +41,7 @@ object CurrentProject: TCENativeProject 'src/imports.d' 'src/mainfun.d' 'src/common.d' + 'src/halstead.d' ) ConfigurationIndex = 1 end diff --git a/dastworx/src/halstead.d b/dastworx/src/halstead.d new file mode 100644 index 00000000..64ec61c5 --- /dev/null +++ b/dastworx/src/halstead.d @@ -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; + } +} + diff --git a/dastworx/src/main.d b/dastworx/src/main.d index 116a0b18..43ad4bb2 100644 --- a/dastworx/src/main.d +++ b/dastworx/src/main.d @@ -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) { diff --git a/src/ce_dastworx.pas b/src/ce_dastworx.pas index 997805de..fa7bbdb4 100644 --- a/src/ce_dastworx.pas +++ b/src/ce_dastworx.pas @@ -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. diff --git a/src/ce_main.lfm b/src/ce_main.lfm index 6e658847..bd099147 100644 --- a/src/ce_main.lfm +++ b/src/ce_main.lfm @@ -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 diff --git a/src/ce_main.pas b/src/ce_main.pas index 5102dcbc..5174a9d0 100644 --- a/src/ce_main.pas +++ b/src/ce_main.pas @@ -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;