From 6e10f889ee76ed05b9b6012dd5a2d835873c26fc Mon Sep 17 00:00:00 2001
From: Basile Burg <basile.b@gmx.com>
Date: Fri, 18 Nov 2016 12:11:10 +0100
Subject: [PATCH] #104, add option cat. for the metrics + put ana in separate
 unit

---
 dastworx/src/halstead.d |   8 +-
 lazproj/coedit.lpi      |   6 +-
 lazproj/coedit.lpr      |   2 +-
 src/ce_halstead.pas     | 288 ++++++++++++++++++++++++++++++++++++++++
 src/ce_main.lfm         |  38 +++++-
 src/ce_main.pas         |  85 +-----------
 6 files changed, 342 insertions(+), 85 deletions(-)
 create mode 100644 src/ce_halstead.pas

diff --git a/dastworx/src/halstead.d b/dastworx/src/halstead.d
index 29d2409d..2ad69319 100644
--- a/dastworx/src/halstead.d
+++ b/dastworx/src/halstead.d
@@ -9,6 +9,11 @@ import
 version(unittest){} else import
     common;
 
+/**
+ * Retrieves the count and unique count of the operands and operators of
+ * each function (inc. methods) of a module. After the call the results are
+ * serialized as JSON in te standard output.
+ */
 void performHalsteadMetrics(const(Module) mod)
 {
     HalsteadMetric hm = construct!(HalsteadMetric);
@@ -91,7 +96,7 @@ private final class HalsteadMetric: ASTVisitor
 
         version(unittest)
         {
-            import std.stdio;
+            import std.stdio: writeln;
             writeln(functions[$-1]);
             writeln('\t',operators);
             writeln('\t',operands);
@@ -479,7 +484,6 @@ version(unittest)
 
     Function test(string source)
     {
-        import std.typecons;
         HalsteadMetric hm = parseAndVisit!(HalsteadMetric)(source);
         scope(exit) destruct(hm);
         return hm.functions[$-1];
diff --git a/lazproj/coedit.lpi b/lazproj/coedit.lpi
index b2b69da8..20c371e8 100644
--- a/lazproj/coedit.lpi
+++ b/lazproj/coedit.lpi
@@ -244,7 +244,7 @@
         <PackageName Value="LCL"/>
       </Item7>
     </RequiredPackages>
-    <Units Count="55">
+    <Units Count="56">
       <Unit0>
         <Filename Value="coedit.lpr"/>
         <IsPartOfProject Value="True"/>
@@ -529,6 +529,10 @@
         <HasResources Value="True"/>
         <ResourceBaseClass Value="Form"/>
       </Unit54>
+      <Unit55>
+        <Filename Value="..\src\ce_halstead.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit55>
     </Units>
   </ProjectOptions>
   <CompilerOptions>
diff --git a/lazproj/coedit.lpr b/lazproj/coedit.lpr
index 4658c3af..55360a28 100644
--- a/lazproj/coedit.lpr
+++ b/lazproj/coedit.lpr
@@ -12,7 +12,7 @@ uses
   ce_dockoptions, ce_shortcutseditor, ce_mru, ce_processes,
   ce_dialogs, ce_dubprojeditor, ce_controls, ce_dfmt, ce_lcldragdrop,
   ce_stringrange, ce_dlangmaps, ce_projgroup, ce_projutils, ce_d2synpresets,
-  ce_dastworx, ce_dbgitf, ce_ddemangle, ce_dubproject;
+  ce_dastworx, ce_dbgitf, ce_ddemangle, ce_dubproject, ce_halstead;
 
 {$R *.res}
 
diff --git a/src/ce_halstead.pas b/src/ce_halstead.pas
new file mode 100644
index 00000000..80ce4972
--- /dev/null
+++ b/src/ce_halstead.pas
@@ -0,0 +1,288 @@
+unit ce_halstead;
+
+{$I ce_defines.inc}
+
+interface
+
+uses
+  Classes, SysUtils, xfpjson, math,
+  ce_common, ce_observer, ce_interfaces, ce_dastworx, ce_writableComponent,
+  ce_synmemo;
+
+type
+
+  TBugCountMethod = (pow23div3000, div3000);
+
+  THalsteadMetricsBase = class(TWritableLfmTextComponent)
+  private
+    fMaxBugsPerFunction: single;
+    fMaxBugsPerModule: single;
+    fMaxVolumePerFunction: integer;
+    fShowAllResults: boolean;
+    fBugCountMethod: TBugCountMethod;
+    procedure setMaxBugsPerFunction(value: single);
+    procedure setMaxBugsPerModule(value: single);
+    procedure setMaxVolumePerFunction(value: integer);
+  published
+    property maxBugsPerFunction: single read fMaxBugsPerFunction write setMaxBugsPerFunction;
+    property maxBugsPerModule: single read fMaxBugsPerModule write setMaxBugsPerModule;
+    property maxVolumePerFunction: integer read fMaxVolumePerFunction write setMaxVolumePerFunction;
+    property showAllResults: boolean read fShowAllResults write fShowAllResults default false;
+    property bugCountMethod: TBugCountMethod read fBugCountMethod write fBugCountMethod default pow23div3000;
+  public
+    constructor create(aOwner: TComponent); override;
+    procedure assign(value: TPersistent); override;
+  end;
+
+  THalsteadMetrics = class(THalsteadMetricsBase, ICEEditableOptions)
+  private
+    fBackup: THalsteadMetricsBase;
+    fMsgs: ICEMessagesDisplay;
+    function optionedWantCategory(): string;
+    function optionedWantEditorKind: TOptionEditorKind;
+    function optionedWantContainer: TPersistent;
+    procedure optionedEvent(event: TOptionEditorEvent);
+    function optionedOptionsModified: boolean;
+  public
+    constructor create(aOwner: TComponent); override;
+    destructor destroy; override;
+    procedure measure(document: TCESynMemo);
+  end;
+
+  function metrics: THalsteadMetrics;
+
+implementation
+
+var
+  fMetrics: THalsteadMetrics = nil;
+const
+  optFname = 'metrics.txt';
+
+function metrics: THalsteadMetrics;
+begin
+  if fMetrics.isNil then
+    fMetrics := THalsteadMetrics.create(nil);
+  result := fMetrics;
+end;
+
+constructor THalsteadMetricsBase.create(aOwner: TComponent);
+begin
+  inherited;
+  fMaxBugsPerFunction:= 0.5;
+  fMaxBugsPerModule:= 2;
+  fMaxVolumePerFunction:= 1000;
+end;
+
+procedure THalsteadMetricsBase.assign(value: TPersistent);
+var
+  s: THalsteadMetricsBase;
+begin
+  if value is THalsteadMetricsBase then
+  begin
+    s := THalsteadMetricsBase(value);
+    fMaxBugsPerFunction:=s.fMaxBugsPerFunction;
+    fMaxBugsPerModule:=s.fMaxBugsPerModule;
+    fMaxVolumePerFunction:=s.fMaxVolumePerFunction;
+    fShowAllResults:=s.fShowAllResults;
+    fBugCountMethod:=s.fBugCountMethod;
+  end
+  else inherited;
+end;
+
+procedure THalsteadMetricsBase.setMaxBugsPerFunction(value: single);
+begin
+  if value < 0 then
+    value := 0;
+  fMaxBugsPerFunction:=value;
+end;
+
+procedure THalsteadMetricsBase.setMaxBugsPerModule(value: single);
+begin
+  if value < 0 then
+    value := 0;
+  fMaxBugsPerModule:=value;
+end;
+
+procedure THalsteadMetricsBase.setMaxVolumePerFunction(value: integer);
+begin
+  if value < 0 then
+    value := 0;
+  fMaxVolumePerFunction:=value;
+end;
+
+constructor THalsteadMetrics.create(aOwner: TComponent);
+var
+  f: string;
+begin
+  inherited;
+  fBackup:= THalsteadMetricsBase.create(self);
+  f := getCoeditDocPath + optFname;
+  if f.fileExists then
+    loadFromFile(f);
+  fBackup.assign(self);
+  EntitiesConnector.addObserver(self);
+end;
+
+destructor THalsteadMetrics.destroy;
+begin
+  EntitiesConnector.removeObserver(self);
+  saveTofile(getCoeditDocPath + optFname);
+  inherited;
+end;
+
+function THalsteadMetrics.optionedWantCategory(): string;
+begin
+  exit('Code metrics');
+end;
+
+function THalsteadMetrics.optionedWantEditorKind: TOptionEditorKind;
+begin
+  exit(oekGeneric);
+end;
+
+function THalsteadMetrics.optionedWantContainer: TPersistent;
+begin
+  fBackup.assign(self);
+  exit(self);
+end;
+
+procedure THalsteadMetrics.optionedEvent(event: TOptionEditorEvent);
+begin
+  case event of
+    oeeAccept: fBackup.assign(self);
+    oeeCancel: assign(fBackup);
+  end;
+end;
+
+function THalsteadMetrics.optionedOptionsModified: boolean;
+begin
+  exit(false);
+end;
+
+procedure THalsteadMetrics.Measure(document: TCESynMemo);
+
+  function checkFunction(const obj: TJSONObject; var bugsSum: single): boolean;
+  var
+    n1, sn1, n2, sn2: integer;
+    val: TJSONData;
+    voc, len, line: integer;
+    vol, dif, eff: single;
+    cpl, bgs: single;
+    vwn: boolean;
+    bwn: boolean;
+  const
+    bgt: array[boolean] of TCEAppMessageKind = (amkInf, amkWarn);
+  begin
+    result := true;
+    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;
+
+    voc := max(1, n1 + n2);
+    len := max(1, sn1 + sn2);
+    cpl := n1*log2(n1) + n2*log2(n2);
+    vol := len * log2(voc);
+    dif := n1 * 0.5 * (sn2 / n2);
+    eff := dif * vol;
+    case fBugCountMethod of
+      pow23div3000: bgs := power(eff, 0.666667) / 3000;
+      div3000: bgs := eff / 3000;
+    end;
+
+    bugsSum += bgs;
+
+    vwn := (fMaxVolumePerFunction <> 0) and not IsNan(vol) and (vol >= fMaxVolumePerFunction);
+    bwn := (fMaxBugsPerFunction <> 0) and not IsNan(bgs) and (bgs >= fMaxBugsPerFunction);
+    result := not vwn and not bwn;
+
+    if fShowAllResults or not result then
+    begin
+      fMsgs.message(format('%s(%d): metrics for "%s"',
+        [document.fileName, line, val.AsString]), document, amcEdit, amkInf);
+
+      fMsgs.message(format('    Vocabulary: %d', [voc]), document, amcEdit, amkInf);
+      fMsgs.message(format('    Length: %d', [len]), document, amcEdit, amkInf);
+      fMsgs.message(format('    Calculated program length: %f', [cpl]), document, amcEdit, amkInf);
+      fMsgs.message(format('    Volume: %.2f', [vol]), document, amcEdit, bgt[vwn]);
+      fMsgs.message(format('    Error proneness: %.2f', [dif]), document, amcEdit, amkInf);
+      fMsgs.message(format('    Implementation effort: %.2f', [eff]), document, amcEdit, amkInf);
+      fMsgs.message(format('    Implementation Time: %.2f secs.', [eff / 18]), document, amcEdit, amkInf);
+      fMsgs.message(format('    Estimated bugs: %.2f', [bgs]), document, amcEdit, bgt[bwn]);
+    end;
+  end;
+
+var
+  jsn: TJSONObject = nil;
+  fnc: TJSONObject = nil;
+  val: TJSONData;
+  arr: TJSONArray;
+  bgS: single = 0;
+  noW: boolean = true;
+  fnW: boolean;
+  i: integer;
+begin
+  if not fShowAllResults
+  and ((maxBugsPerFunction = 0) and (maxBugsPerModule = 0) and (maxVolumePerFunction = 0)) then
+    exit;
+
+  if not assigned(fMsgs) then
+    fMSgs := getMessageDisplay;
+
+  getHalsteadMetrics(document.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
+    begin
+      fnW := checkFunction(fnc, bgS);
+      noW := noW and fnW;
+    end;
+  end;
+
+  if (fMaxBugsPerModule <> 0) and (bgS >= fMaxBugsPerModule) then
+    fMsgs.message(format('The estimated number of bugs (%.2f) in this module exceeds the limit', [bgS]),
+      document, amcEdit, amkWarn)
+  else if noW then
+    fMsgs.message('No abnormal values in the code metrics',
+      document, amcEdit, amkInf);
+
+  jsn.Free;
+end;
+
+initialization
+  fMetrics := THalsteadMetrics.create(nil);
+finalization
+  fMetrics.Free;
+end.
+
diff --git a/src/ce_main.lfm b/src/ce_main.lfm
index bd099147..332ecbf2 100644
--- a/src/ce_main.lfm
+++ b/src/ce_main.lfm
@@ -1468,7 +1468,7 @@ object CEMainForm: TCEMainForm
   OnCloseQuery = FormCloseQuery
   OnDropFiles = FormDropFiles
   ShowHint = True
-  LCLVersion = '1.6.0.4'
+  LCLVersion = '1.6.2.0'
   object mainMenu: TMainMenu
     Images = imgList
     top = 1
@@ -2389,6 +2389,42 @@ object CEMainForm: TCEMainForm
       end
       object MenuItem77: TMenuItem
         Action = actFileMetricsHalstead
+        Bitmap.Data = {
+          36040000424D3604000000000000360000002800000010000000100000000100
+          2000000000000004000064000000640000000000000000000000000000000000
+          0000000000000000000000000000000000000000000000000033000000000000
+          00330000003300000000000000000000000000000000000000002178AD002178
+          AD002178AD002178AD002178AD002177AC00000000332270B3FF00000033008A
+          48FF008A4AFF00000033008E4E00008D4D0000A3620000A160002178AD002178
+          AD002178AD002178AD002177AD00000000331E6EAAFF6FF0FFFF00843CFF00E2
+          9BFF00E39CFF008647FF00000033008C4D0000A3620000A160002178AD002178
+          AD002178AD002177AC00000000331D6FA8FF6DEDFFFF008234FF00E096FF00DE
+          97FF00DE97FF00E29BFF008646FF0000003300A2610000A160002178AD002178
+          AD002177AC00000000331C6EA5FF51C7F9FF00822CFF00E394FF00E196FF007F
+          38FF007F38FF00E096FF00E19BFF008445FF0000003300A160002178AD002177
+          AC00000000331B6DA3FF6BE6FFFF4DD6FFFF52D8FFFF008536FF008334FF55D7
+          FFFF71E1FFFF007E32FF00DF95FF00E09AFF009C5BFF000000332077AC000000
+          00331B6DA3FF4EC1F0FF45D1FFFF45CEFFFF47D4FFFF31351CFF33C1DAFF48D0
+          FFFF47CFFFFF48C2D9FF009648FF00E095FF00E49CFF009F5DFF00000033196D
+          A3FF6FE2FFFF3ECCFFFF3FC9FFFF3FC9FFFF3FCEFFFF3FD3FFFF40CEFFFF3FCA
+          FFFF3FC8FFFF40CAFFFF73DCFFFF008231FF00883CFF00A457001C73A8FF9BF5
+          FFFF36C8FFFF39C4FFFF3BC4FFFF3BC6FFFF39CCFFFF643F30FF39CCFFFF3BC6
+          FFFF3BC4FFFF39C4FFFF38C7FFFF9EF2FFFF206FB1FF2473B5001E75AA00196D
+          A3FF78E0FFFF32C1FFFF34C0FFFF35C2FFFF34CAFFFF563529FF34CAFFFF35C2
+          FFFF34C0FFFF32C1FFFF78E0FFFF196DA4FF1F75AB002177AE002077AC001E74
+          A9001A6DA2FF7CDFFFFF2CBDFFFF2FBEFFFF2FC6FFFF492A1CFF2FC6FFFF2EBE
+          FFFF2CBDFFFF7CDFFFFF1A6DA2FF1E74A9002077AC002178AD002178AD002177
+          AC001E74A9001A6EA2FF7EDFFFFF29BBFFFF2AC0FFFF3C1808FF2AC0FFFF27BA
+          FFFF7DDEFFFF1A6DA2FF1E74A9002177AC002178AD002178AD002178AD002178
+          AD002177AC001E75AA001B6FA3FF52B8F1FF22B9FFFF22BAFFFF21B8FFFF81DD
+          FFFF1A6DA2FF1E74A9002177AC002178AD002178AD002178AD002178AD002178
+          AD002178AD002178AC001F75AA001B6FA3FF83DCFFFF16B2FFFF82DBFFFF1A6E
+          A2FF1E74A9002177AC002178AD002178AD002178AD002178AD002178AD002178
+          AD002178AD002178AD002177AC001E75AA00186EA4FFD0F9FFFF186EA3FF1E75
+          A9002177AC002178AD002178AD002178AD002178AD002178AD002178AD002178
+          AD002178AD002178AD002178AD002077AC001E75AA001B73A8FF1E75AA002077
+          AC002178AD002178AD002178AD002178AD002178AD002178AD00
+        }
       end
       object MenuItem60: TMenuItem
         Action = actFileOpenContFold
diff --git a/src/ce_main.pas b/src/ce_main.pas
index b993f477..1a452f5d 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, math,
+  Graphics, strutils, Dialogs, Menus, ActnList, ExtCtrls, process,
   {$IFDEF WINDOWS}Windows, {$ENDIF} XMLPropStorage, SynExportHTML, fphttpclient,
   xfpjson, xjsonparser, xjsonscanner,
   ce_common, ce_dmdwrap, ce_ceproject, ce_synmemo, ce_writableComponent,
@@ -15,7 +15,8 @@ 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_dastworx;
+  ce_dfmt, ce_lcldragdrop, ce_projgroup, ce_projutils, ce_stringrange, ce_dastworx,
+  ce_halstead;
 
 type
 
@@ -2876,86 +2877,10 @@ begin
 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;
-  bgs: single;
-const
-  bgt: array[boolean] of TCEAppMessageKind = (amkInf, amkWarn);
 begin
-  val := obj.Find('n1Count');
-  if val.isNil then
+  if fDoc.isNil or not fDoc.isDSource 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;
-  bgs := power(eff, 0.666667) / 3000;
-
-  fMsgs.message(format('    Vocabulary: %d', [voc]), fDoc, amcEdit, amkInf);
-  fMsgs.message(format('    Length: %d', [len]), fDoc, amcEdit, amkInf);
-  fMsgs.message(format('    Calculated program length: %f', [n1*log2(n1) + n2*log2(n2)]), fDoc, amcEdit, amkInf);
-  fMsgs.message(format('    Volume: %.2f', [vol]), fDoc, amcEdit, amkInf);
-  fMsgs.message(format('    Difficulty to review: %.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);
-  fMsgs.message(format('    Estimated bugs: %.2f', [bgs]), fDoc, amcEdit, bgt[bgs >= 0.25]);
-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;
+  metrics.measure(fDoc);
 end;
 
 procedure TCEMainForm.actFileNewDubScriptExecute(Sender: TObject);