dexed/src/u_halstead.pas

312 lines
8.0 KiB
Plaintext

unit u_halstead;
{$I u_defines.inc}
interface
uses
Classes, SysUtils, fpjson, math, jsonscanner, jsonparser,
u_common, u_observer, u_interfaces, u_dexed_d, u_writableComponent,
u_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, IEditableOptions)
private
fBackup: THalsteadMetricsBase;
fMsgs: IMessagesDisplay;
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: TDexedMemo);
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;
procedure getHalsteadMetrics(source: TStrings; out jsn: TJSONObject);
var
prs: TJSONParser;
jps: TJSONData;
str: string;
begin
str := halsteadMetrics(PChar(source.Text));
prs := TJSONParser.Create(str, [joIgnoreTrailingComma, joUTF8]);
try
jps := prs.Parse();
if jps.isNotNil and (jps.JSONType = jtObject) then
jsn := TJSONObject(jps.Clone);
jps.Free;
finally
prs.Free;
end;
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 := getDocPath + optFname;
if f.fileExists then
loadFromFile(f);
fBackup.assign(self);
EntitiesConnector.addObserver(self);
end;
destructor THalsteadMetrics.destroy;
begin
EntitiesConnector.removeObserver(self);
saveTofile(getDocPath + 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: TDexedMemo);
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 TAppMessageKind = (amkInf, amkWarn);
begin
result := true;
val := obj.Find('n1Count');
if val.isNil then
exit;
n1 := val.AsInteger;
if n1 = 0 then
exit;
val := obj.Find('n1Sum');
if val.isNil then
exit;
sn1 := val.AsInteger;
val := obj.Find('n2Count');
if val.isNil then
exit;
n2 := val.AsInteger;
if n2 = 0 then
exit;
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;
else bgs := 0;
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.