Skip to content

Commit b36de4a

Browse files
authored
Merge pull request #4 from dpaz/origin/master
Add cyclomatic complexity tool
2 parents c5ec6da + b5d3a31 commit b36de4a

File tree

6 files changed

+144
-3
lines changed

6 files changed

+144
-3
lines changed

Diff for: cmd/bblfsh-tools/cyclomatic.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package main
2+
3+
import "github.com/bblfsh/tools"
4+
5+
type CyclomaticComp struct {
6+
Common
7+
}
8+
9+
func (c *CyclomaticComp) Execute(args []string) error {
10+
return c.execute(args, tools.CyclomaticComplexity{})
11+
}

Diff for: cmd/bblfsh-tools/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func main() {
1515
parser := flags.NewNamedParser("bblfsh-tools", flags.Default)
1616
parser.AddCommand("dummy", "", "Run dummy tool", &Dummy{})
1717
parser.AddCommand("tokenizer", "", "Run tokenizer tool", &Tokenizer{})
18+
parser.AddCommand("cyclomatic", "", "Run cyclomatic complexity tool", &CyclomaticComp{})
1819

1920
if _, err := parser.Parse(); err != nil {
2021
if flagsErr, ok := err.(*flags.Error); ok {

Diff for: cyclomatic.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package tools
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/bblfsh/sdk/uast"
7+
)
8+
9+
// CyclomaticComplexity returns the cyclomatic complexity for the node. The cyclomatic complexity
10+
// is a quantitative measure of the number of linearly independent paths through a program's source code.
11+
// It was developed by Thomas J. McCabe, Sr. in 1976. For a formal description see:
12+
// https://en.wikipedia.org/wiki/Cyclomatic_complexity
13+
// And the original paper: http://www.literateprogramming.com/mccabe.pdf
14+
15+
// This implementation uses PMD implementation as reference and uses the method of
16+
// counting one + one of the following UAST Roles if present on any children:
17+
// If | SwitchCase | For[Each] | [Do]While | TryCatch | Continue | OpBoolean* | Goto
18+
// Important: since some languages allow for code defined
19+
// outside function definitions, this won't check that the Node has the role FunctionDeclarationRole
20+
// so the user should check that if the intended use is calculating the complexity of a function/method.
21+
// If the children contain more than one function definitions, the value will not be averaged between
22+
// the total number of function declarations but given as a total.
23+
//
24+
// Some practical implementations counting tokens in the code. They sometimes differ; for example
25+
// some of them count the switch "default" as an incrementor, some consider all return values minus the
26+
// last, some of them consider "else" (which is wrong IMHO, but not for elifs, remember than the IfElse
27+
// token in the UAST is really an Else not an "else if", elseifs would have a children If token), some
28+
// consider throw and finally while others only the catch, etc.
29+
//
30+
// Examples:
31+
// PMD reference implementation: http://pmd.sourceforge.net/pmd-4.3.0/xref/net/sourceforge/pmd/rules/CyclomaticComplexity.html
32+
// GMetrics: http://gmetrics.sourceforge.net/gmetrics-CyclomaticComplexityMetric.html
33+
// Go: https://github.com/fzipp/gocyclo/blob/master/gocyclo.go#L214
34+
// SonarQube (include rules for many languages): https://docs.sonarqube.org/display/SONAR/Metrics+-+Complexity
35+
//
36+
// IMPORTANT DISCLAIMER: McCabe definition specifies clearly that boolean operations should increment the
37+
// count in 1 for every boolean element when the language if the language evaluates conditions in
38+
// short-circuit. Unfortunately in the current version of the UAST we don't specify these language invariants
39+
// and also we still haven't defined how we are going to represent the boolean expressions (which also would
40+
// need a tree transformation process in the pipeline that we lack) so lacking a better way of detecting for
41+
// all languages what symbols or literals are part of a boolean expression we count the boolean operators
42+
// themselves which should work for short-circuit infix languages but not prefix or infix languages that can
43+
// evaluate more than two items with a single operator. (FIXME when both things are solved in the UAST
44+
// definition and the SDK).
45+
46+
type CyclomaticComplexity struct{}
47+
48+
func (cc CyclomaticComplexity) Exec(n *uast.Node) error {
49+
result := cyclomaticComplexity(n)
50+
fmt.Println("Cyclomatic Complexity = ", result)
51+
return nil
52+
}
53+
54+
func cyclomaticComplexity(n *uast.Node) int {
55+
complexity := 1
56+
57+
iter := uast.NewOrderPathIter(uast.NewPath(n))
58+
59+
for {
60+
p := iter.Next()
61+
if p.IsEmpty() {
62+
break
63+
}
64+
n := p.Node()
65+
for _, r := range n.Roles {
66+
if addsComplexity(r) {
67+
complexity++
68+
}
69+
}
70+
}
71+
return complexity
72+
}
73+
74+
func addsComplexity(r uast.Role) bool {
75+
return r == uast.If || r == uast.SwitchCase || r == uast.For || r == uast.ForEach ||
76+
r == uast.DoWhile || r == uast.While || r == uast.TryCatch || r == uast.Continue ||
77+
r == uast.OpBooleanAnd || r == uast.OpBooleanOr || r == uast.OpBooleanXor
78+
}

Diff for: cyclomatic_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package tools
2+
3+
import (
4+
"testing"
5+
6+
"github.com/bblfsh/sdk/uast"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestCyclomaticComplexity(t *testing.T) {
11+
require := require.New(t)
12+
n := &uast.Node{InternalType: "module",
13+
Children: []*uast.Node{
14+
{InternalType: "root"}, // 1 (initial)
15+
// Prefix is the default so it doesnt need any role
16+
{InternalType: "if1", Roles: []uast.Role{uast.If}, Children: []*uast.Node{ // 2 (If)
17+
{InternalType: "if1else1", Roles: []uast.Role{uast.IfElse}, Children: []*uast.Node{ // 0
18+
{InternalType: "if1else1foreach", Roles: []uast.Role{uast.ForEach}, Children: []*uast.Node{ // 3 (ForEach)
19+
{InternalType: "foreach_child1"}, // 0
20+
{InternalType: "foreach_child2_continue", Roles: []uast.Role{uast.Continue}}, // 4 (Continue)
21+
}},
22+
{InternalType: "if1else1if", Roles: []uast.Role{uast.If}, Children: []*uast.Node{ // 5 (If)
23+
{InternalType: "elseif_child1"}, // 0
24+
{InternalType: "opAnd", Roles: []uast.Role{uast.OpBooleanAnd}}, // 6 (OpBooleanAnd)
25+
{InternalType: "elseif_child2"}, // 0
26+
}},
27+
}},
28+
{InternalType: "break", Roles: []uast.Role{uast.Break}},
29+
},
30+
}}}
31+
require.Equal(cyclomaticComplexity(n), 6)
32+
}

Diff for: glide.lock

+16-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: glide.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ import:
1010
version: v1.2.0
1111
- package: google.golang.org/grpc
1212
version: v1.3.0
13+
- package: srcd.works/go-errors.v0
14+
testImport:
15+
- package: github.com/stretchr/testify
16+
version: v1.1.4
17+
subpackages:
18+
- require

0 commit comments

Comments
 (0)