module halstead; import core.stdc.string; import std.algorithm, std.conv, std.json, std.meta, std.string; import std.stdio, std.ascii, std.digest.crc, std.range: iota; import dparse.ast, dparse.lexer, dparse.parser, dparse.rollback_allocator; import iz.memory, iz.containers, iz.sugar; version(unittest){} else import common; /** * Retrieves the count and unique count of the operands and operators of * each function (inc. member functions) of a module, allowing the compute * the halstead complexity of the functions. * * Params: * src = The source code of the module to analyze, as a C string. * * Returns: a string representing a JSON array named "functions". * Each array item is a JSON object containing * - a function name, named "name" (as JSONString) * - a line number, named "line" (as JSONNumber) * - the count of operators, named "n1count" (as JSONNumber) * - the sum of operators, named "n1sum" (as JSONNumber) * - the count of operands, named "n2count" (as JSONNumber) * - the sum of operands, named "n2sum" (as JSONNumber) */ export extern(C) const(char)* halsteadMetrics(const(char)* src) { LexerConfig config; RollbackAllocator rba; StringCache sCache = StringCache(StringCache.defaultBucketCount); scope mod = src[0 .. src.strlen] .getTokensForParser(config, &sCache) .parseModule("", &rba, &ignoreErrors); HalsteadMetric hm = construct!(HalsteadMetric); scope (exit) destruct(hm); hm.visit(mod); return hm.serialize(); } private struct Function { size_t line; string name; size_t N1, n1; size_t N2, n2; alias operatorsSum = N1; alias operatorsKinds = n1; alias operandsSum = N2; alias operandsKinds = n2; } private struct BinaryExprFlags { bool leftIsFunction; bool rightIsFunction; } private final class HalsteadMetric: ASTVisitor { alias visit = ASTVisitor.visit; Function[] functions; HashMap_AB!(string, size_t) operators; HashMap_AB!(string, size_t) operands; BinaryExprFlags[] binExprFlag; size_t functionNesting; bool[] inFunctionCallChain; const(IdentifierOrTemplateInstance)[] chain; bool ifStatement; JSONValue fs; void processCallChain() { if (chain.length) { static Token getIdent(const(IdentifierOrTemplateInstance) i) { if (i.identifier != tok!"") return i.identifier; else return i.templateInstance.identifier; } foreach(i, ident; chain) { if (i == chain.length-1) operators[getIdent(ident).text] +=1; else operands[getIdent(ident).text] +=1; } chain.length = 0; } } void addOperandFromToken(const ref Token tk) { if (isLiteral(tk.type)) { alias immutHexStr = toHexString!(Order.increasing, LetterCase.upper); operands["literal" ~ immutHexStr(tk.text.crc32Of)] +=1; } else operands[tk.text] +=1; } void pushExprFlags(bool leftFlag = false, bool rightFlag = false) { binExprFlag.length += 1; binExprFlag[$-1].leftIsFunction = leftFlag; binExprFlag[$-1].rightIsFunction = rightFlag; } void popExprFlags() { binExprFlag.length -= 1; } bool exprLeftIsFunction(){return binExprFlag[$-1].leftIsFunction;} bool exprRightIsFunction(){return binExprFlag[$-1].rightIsFunction;} this() { fs = parseJSON("[]"); pushExprFlags; inFunctionCallChain.length++; } const(char)* serialize() { JSONValue js; js["functions"] = fs; return js.toString.toStringz(); } override void visit(const(PragmaExpression)){} override void visit(const(Unittest)){} void beginFunction() { operators.clear; operands.clear; operators.reserve(64); operands.reserve(64); if (functionNesting++ == 0) functions.length = functions.length + 1; } void endFunction(string name, size_t line) { functions[$-1].name = name; functions[$-1].line = line; if (operators.count) { functions[$-1].N1 = operators.byValue.fold!((a,b) => b = a + b); functions[$-1].n1 = operators.count; } if (operands.count) { functions[$-1].N2 = operands.byValue.fold!((a,b) => b = a + b); functions[$-1].n2 = operands.count; } 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) { writeln(functions[$-1]); writeln("\toperators: ",operators); writeln("\toperands : ",operands); } functionNesting--; } override void visit(const(TemplateArguments) ta) { ta.accept(this); if (ta.templateSingleArgument) addOperandFromToken(ta.templateSingleArgument.token); } override void visit(const(FunctionCallExpression) expr) { inFunctionCallChain.length++; inFunctionCallChain[$-1] = true; if (const TemplateSingleArgument tsi = safeAccess(expr) .templateArguments.templateSingleArgument) addOperandFromToken(tsi.token); expr.accept(this); if (inFunctionCallChain[$-1]) { processCallChain; } inFunctionCallChain.length--; } override void visit(const(FunctionDeclaration) decl) { beginFunction; if (!decl.functionBody) return; decl.accept(this); endFunction(patchPascalString(funcDeclText(decl)), decl.name.line); } void visitFunction(T)(const(T) decl) { beginFunction; decl.accept(this); endFunction(T.stringof ~ to!string(decl.line), decl.line); } static string funDeclString() { alias FunDecl = AliasSeq!( SharedStaticConstructor, StaticConstructor, Constructor, SharedStaticDestructor, StaticDestructor, Destructor, Postblit ); string result; enum funDeclOverride(T) = "override void visit(const(" ~ T.stringof ~ ") decl) { visitFunction(decl); }"; foreach(i; aliasSeqOf!(iota(0,FunDecl.length))) { result ~= funDeclOverride!(FunDecl[i]); } return result; } mixin(funDeclString); override void visit(const(PrimaryExpression) primary) { if (primary.identifierOrTemplateInstance !is null) { if (inFunctionCallChain[$-1]) chain ~= primary.identifierOrTemplateInstance; if ((!inFunctionCallChain[$-1]) || (inFunctionCallChain[$-1] & exprLeftIsFunction) || (inFunctionCallChain[$-1] & exprRightIsFunction)) { operands[primary.identifierOrTemplateInstance.identifier.text] +=1; } } else addOperandFromToken(primary.primary); primary.accept(this); } override void visit(const(ArgumentList) al) { if (inFunctionCallChain[$-1]) processCallChain; inFunctionCallChain[$-1] = false; al.accept(this); } override void visit(const(UnaryExpression) expr) { expr.accept(this); if (expr.identifierOrTemplateInstance) { operators["."] += 1; if (inFunctionCallChain[$-1]) chain ~= expr.identifierOrTemplateInstance; else { if (expr.identifierOrTemplateInstance.identifier != tok!"") operands[expr.identifierOrTemplateInstance.identifier.text] +=1; else operands[expr.identifierOrTemplateInstance.templateInstance.identifier.text] +=1; } } if (expr.prefix.type) operators[str(expr.prefix.type)] += 1; if (expr.suffix.type) operators[str(expr.suffix.type)] += 1; } override void visit(const(AsmInstruction) ai) { if (ai.identifierOrIntegerOrOpcode != tok!"") { operators[ai.identifierOrIntegerOrOpcode.text] += 1; } ai.accept(this); } override void visit(const(Register) reg) { if (reg.identifier != tok!"") { operands[reg.identifier.text] +=1; } if (reg.hasIntegerLiteral) { addOperandFromToken(reg.intLiteral); } reg.accept(this); } override void visit(const(AsmPrimaryExp) ape) { if (ape.token != tok!"") addOperandFromToken(ape.token); if (ape.identifierChain) ape.identifierChain.identifiers .filter!(a => !a.text.among("dword","ptr")) .each!(a => addOperandFromToken(a)); ape.accept(this); } override void visit(const(IndexExpression) expr) { operators["[]"] += 1; expr.accept(this); } override void visit(const(NewExpression) expr) { operators["new"] += 1; expr.accept(this); } override void visit(const(NewAnonClassExpression) expr) { operators["new"] += 1; expr.accept(this); } override void visit(const(DeleteExpression) expr) { operators["delete"] += 1; expr.accept(this); } override void visit(const(CastExpression) expr) { operators["cast"] += 1; expr.accept(this); } override void visit(const(IsExpression) expr) { operators["is"] += 1; expr.accept(this); } override void visit(const(TernaryExpression) expr) { if (expr.orOrExpression) operators["if"] += 1; if (expr.expression) operators["else"] += 1; expr.accept(this); } override void visit(const(TypeidExpression) expr) { operators["typeid"] += 1; expr.accept(this); } override void visit(const(IfStatement) st) { operators["if"] += 1; st.accept(this); if (st.thenStatement) operators["then"] += 1; if (st.elseStatement) operators["else"] += 1; } override void visit(const(WhileStatement) st) { operators["while"] +=1; st.accept(this); } override void visit(const(ForStatement) st) { operators["for"] +=1; st.accept(this); } override void visit(const(ForeachStatement) st) { operators["foreach"] +=1; if (st.foreachTypeList) foreach(ft; st.foreachTypeList.items) operands[ft.identifier.text] +=1; if (st.foreachType) operands[st.foreachType.identifier.text] +=1; st.accept(this); } override void visit(const(ReturnStatement) st) { operators["return"] +=1; st.accept(this); } override void visit(const(BreakStatement) st) { operators["break"] +=1; st.accept(this); } override void visit(const(ContinueStatement) st) { operators["continue"] +=1; st.accept(this); } override void visit(const(GotoStatement) st) { operators["goto"] +=1; st.accept(this); } override void visit(const(SwitchStatement) st) { operators["switch"] +=1; st.accept(this); } override void visit(const(CaseStatement) st) { operators["case"] +=1; st.accept(this); } override void visit(const(CaseRangeStatement) st) { operators["case"] +=2; st.accept(this); } override void visit(const(DefaultStatement) st) { operators["case"] +=1; st.accept(this); } override void visit(const(ThrowStatement) st) { operators["throw"] +=1; st.accept(this); } override void visit(const(TryStatement) st) { operators["try"] +=1; st.accept(this); } override void visit(const(Catch) c) { operators["catch"] +=1; c.accept(this); if (c.identifier.text) operands[c.identifier.text] +=1; } override void visit(const(VariableDeclaration) decl) { if (decl.declarators) foreach (elem; decl.declarators) { operands[elem.name.text] +=1; if (elem.initializer) operators["="] +=1; } else if (decl.autoDeclaration) visit(decl.autoDeclaration); decl.accept(this); } override void visit(const AutoDeclarationPart decl) { operands[decl.identifier.text] +=1; operators["="] +=1; decl.accept(this); } void visitBinExpr(T)(const(T) expr) { bool leftArgIsFunctFlag; bool rightArgIsFunctFlag; static if (__traits(hasMember, T, "left")) { if (expr.left && (cast(UnaryExpression) expr.left) && (cast(UnaryExpression) expr.left).functionCallExpression) leftArgIsFunctFlag = true; } static if (__traits(hasMember, T, "right")) { if (expr.right && (cast(UnaryExpression) expr.right) && (cast(UnaryExpression) expr.right).functionCallExpression) rightArgIsFunctFlag = true; } string op; static if (__traits(hasMember, T, "operator")) { op = str(expr.operator); } else { static if (is(T == AndExpression)) op = `&`; else static if (is(T == AndAndExpression)) op = `&&`; else static if (is(T == AsmAndExp)) op = `&`; else static if (is(T == AsmLogAndExp)) op = "&&"; else static if (is(T == AsmLogOrExp)) op = "||"; else static if (is(T == AsmOrExp)) op = "|"; else static if (is(T == AsmXorExp)) op = "|"; else static if (is(T == IdentityExpression)) op = expr.negated ? "!is" : "is"; else static if (is(T == InExpression)) op = expr.negated ? "!in" : "in"; else static if (is(T == OrExpression)) op = `|`; else static if (is(T == OrOrExpression)) op = `||`; else static if (is(T == PowExpression)) op = `^^`; else static if (is(T == XorExpression)) op = `^`; else static assert(0, T.stringof); } operators[op] +=1; pushExprFlags(leftArgIsFunctFlag, rightArgIsFunctFlag); expr.accept(this); popExprFlags; } static string binExprsString() { alias SeqOfBinExpr = AliasSeq!( AddExpression, AndExpression, AndAndExpression, AsmAddExp, AsmAndExp, AsmEqualExp, AsmLogAndExp, AsmLogOrExp, AsmMulExp, AsmOrExp, AsmRelExp, AsmShiftExp, AsmXorExp, AssignExpression, EqualExpression, IdentityExpression, InExpression, MulExpression, OrExpression, OrOrExpression, PowExpression, RelExpression, ShiftExpression, XorExpression, ); enum binExpOverride(T) = "override void visit(const(" ~ T.stringof ~ ") expr) { visitBinExpr(expr); }"; string result; foreach(i; aliasSeqOf!(iota(0, SeqOfBinExpr.length))) result ~= binExpOverride!(SeqOfBinExpr[i]); return result; } mixin(binExprsString()); } 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; } Function test(string source) { HalsteadMetric hm = parseAndVisit!(HalsteadMetric)(source); scope(exit) destruct(hm); return hm.functions[$-1]; } } unittest { Function r = q{ void foo() { Object o = new Object; } }.test; assert(r.operandsKinds == 1); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { auto o = new Object; } }.test; assert(r.operandsKinds == 1); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { auto o = 1 + 2; } }.test; assert(r.operandsKinds == 3); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { foo(bar,baz); } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { int i = foo(bar,baz) + foo(bar,baz); } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 3); } unittest { Function r = q{ void foo() { bar!("lit")(a); } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { enum E{e0} E e; bar!(e,"lit")(baz(e)); } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo(); }.test; assert(r.operandsKinds == 0); assert(r.operatorsKinds == 0); } unittest { Function r = q{ shared static this() { int i = 0; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ shared static ~this() { int i = 0; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ static this() { int i = 0; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ static ~this() { int i = 0; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ class Foo { this() { int i = 0; } } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ class Foo { ~this() { int i = 0; } } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { i += a << b.c; } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 3); } unittest { Function r = q{ void foo() { ++h; i--; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { ++i--; } }.test; assert(r.operandsKinds == 1); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { i = a | b & c && d || e^f + g^^h - a in b + a[0]; } }.test; assert(r.operandsKinds == 10); assert(r.operatorsKinds == 11); } unittest { Function r = q{ void foo() { Bar bar = new Bar; auto baz = cast(Baz) bar; delete bar; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 4); } unittest { Function r = q{ void foo() { foreach(i,a;z){} } }.test; assert(r.operandsKinds == 3); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { foreach(i; l..h){} } }.test; assert(r.operandsKinds == 3); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { for(i = 0; i < len; i++){} } }.test; assert(r.operandsKinds == 3); assert(r.operatorsKinds == 4); } unittest { Function r = q{ void foo() { for(;;){continue;} } }.test; assert(r.operandsKinds == 0); assert(r.operatorsKinds == 2); } unittest { Function r = q{ int foo() { while(true) {return 0;} } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { switch(a) { default: break; case 1: return; case 2: .. case 8: ; } } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 4); } unittest { Function r = q{ void foo() { try a(); catch(Exception e) throw v; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 4); } unittest { Function r = q{ void foo() { if (true) {} else {i = 0;} } }.test; assert(r.operandsKinds == 3); assert(r.operatorsKinds == 4); } unittest { Function r = q{ void foo() { a = true ? 0 : 1; } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 3); } version(none) unittest { // TODO: detect function call w/o parens Function r = q{ void foo() { bar; } }.test; assert(r.operandsKinds == 0); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { a = bar(b) + baz(z); } }.test; assert(r.operandsKinds == 5); assert(r.operatorsKinds == 4); } unittest { Function r = q{ void foo() { a = bar(cat(0) - dog(1)) + baz(z); } }.test; assert(r.operandsKinds == 8); assert(r.operatorsKinds == 7); } unittest { Function r = q{ void foo() { a = bar(cat(0) && dog(1)) | baz(z); } }.test; assert(r.operandsKinds == 8); assert(r.operatorsKinds == 7); } unittest { Function r = q{ void foo() { a = bar(c)++; } }.test; // would be 3 by considering bar as an operand // but this is actually (almost always) invalid code. assert(r.operandsKinds == 2); assert(r.operatorsKinds == 3); } unittest { Function r = q{ void foo() { a = !!!a; } }.test; assert(r.operandsKinds == 1); assert(r.operatorsKinds == 2); assert(r.operatorsSum == 4); } unittest { Function r = q{ void foo() { a = b[foo(a)]; } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 3); } unittest { Function r = q{ this(){a = 0;} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ static this(){a = 0;} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ shared static this(){a = 0;} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ ~this(){a = 0;} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ static ~this(){a = 0;} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ shared static ~this(){a = 0;} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ struct S{this(this){a = 0;}} }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ struct S{@disable this(this);} }.test; assert(r.operandsKinds == 0); assert(r.operatorsKinds == 0); } unittest { Function r = q{ void foo() { a.b.c = d.e; } }.test; assert(r.operandsKinds == 5); assert(r.operatorsKinds == 2); assert(r.operatorsSum == 4); } unittest { Function r = q{ void foo() { a.b.c(d.e); } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 2); assert(r.operatorsSum == 4); } unittest { Function r = q{ void foo() { a.b.c.d(e.f()); } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 3); } unittest { Function r = q{ void foo() { a.b!(8,9).c = f; } }.test; assert(r.operandsKinds == 6); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { a.b!8.c = f; } }.test; assert(r.operandsKinds == 5); assert(r.operatorsKinds == 2); } unittest { Function r = q{ void foo() { asm{xor EAX,ECX;} } }.test; assert(r.operandsKinds == 2); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { asm{mov EAX, SS:CL;} } }.test; assert(r.operandsKinds == 3); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { asm{mov EAX, a.b.c;} } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 1); } unittest { Function r = q{ void foo() { asm{imul EAX, dword [EBP + EBX * 4 + 0x0FFFFFFD4];} } }.test; assert(r.operandsKinds == 5); assert(r.operatorsKinds == 3); } unittest { Function r = q{ void foo() { asm{imul EAX, ECX, dword[ESP + 8];} } }.test; assert(r.operandsKinds == 4); assert(r.operatorsKinds == 2); }