From d5b98c82203ef160f791e4ade7d5ed360bce6c1e Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Fri, 6 Dec 2024 20:38:44 +0700 Subject: [PATCH 1/9] Add tracing capability --- cmd/gocognit/main.go | 23 +++++----- gocognit.go | 100 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/cmd/gocognit/main.go b/cmd/gocognit/main.go index 469c3b2..c14893f 100644 --- a/cmd/gocognit/main.go +++ b/cmd/gocognit/main.go @@ -110,6 +110,7 @@ func main() { format string jsonEncode bool ignoreExpr string + trace bool ) flag.IntVar(&over, "over", defaultOverFlagVal, "show functions with complexity > N only") @@ -119,6 +120,7 @@ func main() { flag.StringVar(&format, "f", defaultFormat, "the format to use") flag.BoolVar(&jsonEncode, "json", false, "encode the output as JSON") flag.StringVar(&ignoreExpr, "ignore", "", "ignore files matching the given regexp") + flag.BoolVar(&trace, "trace", false, "include trace information of the complexity") log.SetFlags(0) log.SetPrefix("gocognit: ") @@ -136,7 +138,8 @@ func main() { log.Fatal(err) } - stats, err := analyze(args, includeTests) + traceEnabled := trace && jsonEncode + stats, err := analyze(args, includeTests, traceEnabled) if err != nil { log.Fatal(err) } @@ -170,19 +173,19 @@ func main() { } } -func analyzePath(path string, includeTests bool) ([]gocognit.Stat, error) { +func analyzePath(path string, includeTests bool, trace bool) ([]gocognit.Stat, error) { if isDir(path) { - return analyzeDir(path, includeTests, nil) + return analyzeDir(path, includeTests, nil, trace) } - return analyzeFile(path, nil) + return analyzeFile(path, nil, trace) } -func analyze(paths []string, includeTests bool) (stats []gocognit.Stat, err error) { +func analyze(paths []string, includeTests bool, trace bool) (stats []gocognit.Stat, err error) { var out []gocognit.Stat for _, path := range paths { - stats, err := analyzePath(path, includeTests) + stats, err := analyzePath(path, includeTests, trace) if err != nil { return nil, err } @@ -199,7 +202,7 @@ func isDir(filename string) bool { return err == nil && fi.IsDir() } -func analyzeFile(fname string, stats []gocognit.Stat) ([]gocognit.Stat, error) { +func analyzeFile(fname string, stats []gocognit.Stat, trace bool) ([]gocognit.Stat, error) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments) @@ -207,10 +210,10 @@ func analyzeFile(fname string, stats []gocognit.Stat) ([]gocognit.Stat, error) { return nil, err } - return gocognit.ComplexityStats(f, fset, stats), nil + return gocognit.ComplexityStats(f, fset, stats, trace), nil } -func analyzeDir(dirname string, includeTests bool, stats []gocognit.Stat) ([]gocognit.Stat, error) { +func analyzeDir(dirname string, includeTests bool, stats []gocognit.Stat, trace bool) ([]gocognit.Stat, error) { err := filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -228,7 +231,7 @@ func analyzeDir(dirname string, includeTests bool, stats []gocognit.Stat) ([]goc return nil } - stats, err = analyzeFile(path, stats) + stats, err = analyzeFile(path, stats, trace) if err != nil { return err } diff --git a/gocognit.go b/gocognit.go index 126452e..01af2df 100644 --- a/gocognit.go +++ b/gocognit.go @@ -15,6 +15,7 @@ type Stat struct { PkgName string FuncName string Complexity int + Traces []Trace `json:",omitempty"` Pos token.Position } @@ -23,7 +24,7 @@ func (s Stat) String() string { } // ComplexityStats builds the complexity statistics. -func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat) []Stat { +func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat, includeTrace bool) []Stat { for _, decl := range f.Decls { if fn, ok := decl.(*ast.FuncDecl); ok { d := parseDirective(fn.Doc) @@ -31,10 +32,13 @@ func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat) []Stat { continue } + res := Scan(fn, includeTrace) + stats = append(stats, Stat{ PkgName: f.Name.Name, FuncName: funcName(fn), - Complexity: Complexity(fn), + Complexity: res.Complexity, + Traces: res.Traces, Pos: fset.Position(fn.Pos()), }) } @@ -77,13 +81,44 @@ func funcName(fn *ast.FuncDecl) string { // Complexity calculates the cognitive complexity of a function. func Complexity(fn *ast.FuncDecl) int { + res := Scan(fn, false) + + return res.Complexity +} + +// Scan scans the function declaration. +func Scan(fn *ast.FuncDecl, trace bool) Result { v := complexityVisitor{ - name: fn.Name, + name: fn.Name, + trace: trace, } ast.Walk(&v, fn) - return v.complexity + return Result{ + Traces: v.traces, + Complexity: v.complexity, + } +} + +type Result struct { + Traces []Trace + Complexity int +} + +type Trace struct { + Inc int + Nesting int `json:",omitempty"` + Text string + Pos token.Pos +} + +func (t Trace) String() string { + if t.Nesting == 0 { + return fmt.Sprintf("+%d", t.Inc) + } + + return fmt.Sprintf("+%d (nesting=%d)", t.Inc, t.Nesting) } type complexityVisitor struct { @@ -92,6 +127,11 @@ type complexityVisitor struct { nesting int elseNodes map[ast.Node]bool calculatedExprs map[ast.Expr]bool + + fset *token.FileSet + + trace bool + traces []Trace } func (v *complexityVisitor) incNesting() { @@ -102,12 +142,33 @@ func (v *complexityVisitor) decNesting() { v.nesting-- } -func (v *complexityVisitor) incComplexity() { +func (v *complexityVisitor) incComplexity(text string, pos token.Pos) { v.complexity++ + + if !v.trace { + return + } + + v.traces = append(v.traces, Trace{ + Inc: 1, + Text: text, + Pos: pos, + }) } -func (v *complexityVisitor) nestIncComplexity() { +func (v *complexityVisitor) nestIncComplexity(text string, pos token.Pos) { v.complexity += (v.nesting + 1) + + if !v.trace { + return + } + + v.traces = append(v.traces, Trace{ + Inc: v.nesting + 1, + Nesting: v.nesting, + Text: text, + Pos: pos, + }) } func (v *complexityVisitor) markAsElseNode(n ast.Node) { @@ -171,7 +232,7 @@ func (v *complexityVisitor) Visit(n ast.Node) ast.Visitor { } func (v *complexityVisitor) visitIfStmt(n *ast.IfStmt) ast.Visitor { - v.incIfComplexity(n) + v.incIfComplexity(n, "if", n.Pos()) if n := n.Init; n != nil { ast.Walk(v, n) @@ -184,7 +245,7 @@ func (v *complexityVisitor) visitIfStmt(n *ast.IfStmt) ast.Visitor { v.decNesting() if _, ok := n.Else.(*ast.BlockStmt); ok { - v.incComplexity() + v.incComplexity("else", n.Else.Pos()) ast.Walk(v, n.Else) } else if _, ok := n.Else.(*ast.IfStmt); ok { @@ -196,7 +257,7 @@ func (v *complexityVisitor) visitIfStmt(n *ast.IfStmt) ast.Visitor { } func (v *complexityVisitor) visitSwitchStmt(n *ast.SwitchStmt) ast.Visitor { - v.nestIncComplexity() + v.nestIncComplexity("switch", n.Pos()) if n := n.Init; n != nil { ast.Walk(v, n) @@ -214,7 +275,7 @@ func (v *complexityVisitor) visitSwitchStmt(n *ast.SwitchStmt) ast.Visitor { } func (v *complexityVisitor) visitTypeSwitchStmt(n *ast.TypeSwitchStmt) ast.Visitor { - v.nestIncComplexity() + v.nestIncComplexity("switch", n.Pos()) if n := n.Init; n != nil { ast.Walk(v, n) @@ -232,7 +293,7 @@ func (v *complexityVisitor) visitTypeSwitchStmt(n *ast.TypeSwitchStmt) ast.Visit } func (v *complexityVisitor) visitSelectStmt(n *ast.SelectStmt) ast.Visitor { - v.nestIncComplexity() + v.nestIncComplexity("select", n.Pos()) v.incNesting() ast.Walk(v, n.Body) @@ -242,7 +303,7 @@ func (v *complexityVisitor) visitSelectStmt(n *ast.SelectStmt) ast.Visitor { } func (v *complexityVisitor) visitForStmt(n *ast.ForStmt) ast.Visitor { - v.nestIncComplexity() + v.nestIncComplexity("for", n.Pos()) if n := n.Init; n != nil { ast.Walk(v, n) @@ -264,7 +325,7 @@ func (v *complexityVisitor) visitForStmt(n *ast.ForStmt) ast.Visitor { } func (v *complexityVisitor) visitRangeStmt(n *ast.RangeStmt) ast.Visitor { - v.nestIncComplexity() + v.nestIncComplexity("for", n.Pos()) if n := n.Key; n != nil { ast.Walk(v, n) @@ -294,7 +355,7 @@ func (v *complexityVisitor) visitFuncLit(n *ast.FuncLit) ast.Visitor { func (v *complexityVisitor) visitBranchStmt(n *ast.BranchStmt) ast.Visitor { if n.Label != nil { - v.incComplexity() + v.incComplexity(n.Tok.String(), n.Pos()) } return v } @@ -306,8 +367,7 @@ func (v *complexityVisitor) visitBinaryExpr(n *ast.BinaryExpr) ast.Visitor { var lastOp token.Token for _, op := range ops { if lastOp != op { - v.incComplexity() - + v.incComplexity(op.String(), n.OpPos) lastOp = op } } @@ -321,7 +381,7 @@ func (v *complexityVisitor) visitCallExpr(n *ast.CallExpr) ast.Visitor { obj, name := callIdent.Obj, callIdent.Name if obj == v.name.Obj && name == v.name.Name { // called by same function directly (direct recursion) - v.incComplexity() + v.incComplexity(name, n.Pos()) } } @@ -337,11 +397,11 @@ func (v *complexityVisitor) collectBinaryOps(exp ast.Expr) []token.Token { return nil } -func (v *complexityVisitor) incIfComplexity(n *ast.IfStmt) { +func (v *complexityVisitor) incIfComplexity(n *ast.IfStmt, text string, pos token.Pos) { if v.markedAsElseNode(n) { - v.incComplexity() + v.incComplexity(text, pos) } else { - v.nestIncComplexity() + v.nestIncComplexity(text, pos) } } From 8fb2033ae3c84151c17488301dc755f48254933a Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Fri, 6 Dec 2024 22:45:35 +0700 Subject: [PATCH 2/9] Add Position to the Trace --- gocognit.go | 63 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/gocognit.go b/gocognit.go index 01af2df..d1f6712 100644 --- a/gocognit.go +++ b/gocognit.go @@ -19,6 +19,21 @@ type Stat struct { Pos token.Position } +type Trace struct { + Inc int + Nesting int `json:",omitempty"` + Text string + Pos token.Position +} + +func (t Trace) String() string { + if t.Nesting == 0 { + return fmt.Sprintf("+%d", t.Inc) + } + + return fmt.Sprintf("+%d (nesting=%d)", t.Inc, t.Nesting) +} + func (s Stat) String() string { return fmt.Sprintf("%d %s %s %s", s.Complexity, s.PkgName, s.FuncName, s.Pos) } @@ -32,13 +47,13 @@ func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat, includeTrac continue } - res := Scan(fn, includeTrace) + res := ScanComplexity(fn, includeTrace) stats = append(stats, Stat{ PkgName: f.Name.Name, FuncName: funcName(fn), Complexity: res.Complexity, - Traces: res.Traces, + Traces: traces(fset, res.Traces), Pos: fset.Position(fn.Pos()), }) } @@ -47,6 +62,20 @@ func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat, includeTrac return stats } +func traces(fset *token.FileSet, traces []trace) []Trace { + tags := make([]Trace, 0, len(traces)) + for _, t := range traces { + tags = append(tags, Trace{ + Inc: t.Inc, + Nesting: t.Nesting, + Text: t.Text, + Pos: fset.Position(t.Pos), + }) + } + + return tags +} + type directive struct { Ignore bool } @@ -81,13 +110,13 @@ func funcName(fn *ast.FuncDecl) string { // Complexity calculates the cognitive complexity of a function. func Complexity(fn *ast.FuncDecl) int { - res := Scan(fn, false) + res := ScanComplexity(fn, false) return res.Complexity } -// Scan scans the function declaration. -func Scan(fn *ast.FuncDecl, trace bool) Result { +// ScanComplexity scans the function declaration. +func ScanComplexity(fn *ast.FuncDecl, trace bool) ScanResult { v := complexityVisitor{ name: fn.Name, trace: trace, @@ -95,32 +124,24 @@ func Scan(fn *ast.FuncDecl, trace bool) Result { ast.Walk(&v, fn) - return Result{ + return ScanResult{ Traces: v.traces, Complexity: v.complexity, } } -type Result struct { - Traces []Trace +type ScanResult struct { + Traces []trace Complexity int } -type Trace struct { +type trace struct { Inc int - Nesting int `json:",omitempty"` + Nesting int Text string Pos token.Pos } -func (t Trace) String() string { - if t.Nesting == 0 { - return fmt.Sprintf("+%d", t.Inc) - } - - return fmt.Sprintf("+%d (nesting=%d)", t.Inc, t.Nesting) -} - type complexityVisitor struct { name *ast.Ident complexity int @@ -131,7 +152,7 @@ type complexityVisitor struct { fset *token.FileSet trace bool - traces []Trace + traces []trace } func (v *complexityVisitor) incNesting() { @@ -149,7 +170,7 @@ func (v *complexityVisitor) incComplexity(text string, pos token.Pos) { return } - v.traces = append(v.traces, Trace{ + v.traces = append(v.traces, trace{ Inc: 1, Text: text, Pos: pos, @@ -163,7 +184,7 @@ func (v *complexityVisitor) nestIncComplexity(text string, pos token.Pos) { return } - v.traces = append(v.traces, Trace{ + v.traces = append(v.traces, trace{ Inc: v.nesting + 1, Nesting: v.nesting, Text: text, From 1faed928774534d8398c5e7cc7b384c5cecc982d Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 07:15:28 +0700 Subject: [PATCH 3/9] Report diagnostic position --- cmd/gocognit/main.go | 46 ++++++++++------ gocognit.go | 125 +++++++++++++++++++++++++++++-------------- 2 files changed, 115 insertions(+), 56 deletions(-) diff --git a/cmd/gocognit/main.go b/cmd/gocognit/main.go index c14893f..6a18854 100644 --- a/cmd/gocognit/main.go +++ b/cmd/gocognit/main.go @@ -66,8 +66,9 @@ Flags: not depending on whether -over or -top are set -test indicates whether test files should be included -json encode the output as JSON + -d enable diagnostic output -f format string the format to use - (default "{{.PkgName}}.{{.FuncName}}:{{.Complexity}}:{{.Pos}}") + (default "{{.Complexity}} {{.PkgName}} {{.FuncName}} {{.Pos}}") The (default) output fields for each line are: @@ -82,10 +83,24 @@ or equal to The struct being passed to the template is: type Stat struct { - PkgName string - FuncName string - Complexity int - Pos token.Position + PkgName string + FuncName string + Complexity int + Diagnostics []Diagnostics + Pos token.Position + } + + type Diagnostic struct { + Inc string + Nesting int + Text string + Pos DiagnosticPosition + } + + type DiagnosticPosition struct { + Offset int + Line int + Column int } ` @@ -110,7 +125,7 @@ func main() { format string jsonEncode bool ignoreExpr string - trace bool + diagnostic bool ) flag.IntVar(&over, "over", defaultOverFlagVal, "show functions with complexity > N only") @@ -120,7 +135,7 @@ func main() { flag.StringVar(&format, "f", defaultFormat, "the format to use") flag.BoolVar(&jsonEncode, "json", false, "encode the output as JSON") flag.StringVar(&ignoreExpr, "ignore", "", "ignore files matching the given regexp") - flag.BoolVar(&trace, "trace", false, "include trace information of the complexity") + flag.BoolVar(&diagnostic, "d", false, "enable diagnostic output") log.SetFlags(0) log.SetPrefix("gocognit: ") @@ -138,8 +153,7 @@ func main() { log.Fatal(err) } - traceEnabled := trace && jsonEncode - stats, err := analyze(args, includeTests, traceEnabled) + stats, err := analyze(args, includeTests, diagnostic) if err != nil { log.Fatal(err) } @@ -173,19 +187,19 @@ func main() { } } -func analyzePath(path string, includeTests bool, trace bool) ([]gocognit.Stat, error) { +func analyzePath(path string, includeTests bool, includeDiagnostic bool) ([]gocognit.Stat, error) { if isDir(path) { - return analyzeDir(path, includeTests, nil, trace) + return analyzeDir(path, includeTests, nil, includeDiagnostic) } - return analyzeFile(path, nil, trace) + return analyzeFile(path, nil, includeDiagnostic) } -func analyze(paths []string, includeTests bool, trace bool) (stats []gocognit.Stat, err error) { +func analyze(paths []string, includeTests bool, includeDiagnostic bool) (stats []gocognit.Stat, err error) { var out []gocognit.Stat for _, path := range paths { - stats, err := analyzePath(path, includeTests, trace) + stats, err := analyzePath(path, includeTests, includeDiagnostic) if err != nil { return nil, err } @@ -202,7 +216,7 @@ func isDir(filename string) bool { return err == nil && fi.IsDir() } -func analyzeFile(fname string, stats []gocognit.Stat, trace bool) ([]gocognit.Stat, error) { +func analyzeFile(fname string, stats []gocognit.Stat, includeDiagnostic bool) ([]gocognit.Stat, error) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments) @@ -210,7 +224,7 @@ func analyzeFile(fname string, stats []gocognit.Stat, trace bool) ([]gocognit.St return nil, err } - return gocognit.ComplexityStats(f, fset, stats, trace), nil + return gocognit.ComplexityStatsWithDiagnostic(f, fset, stats, includeDiagnostic), nil } func analyzeDir(dirname string, includeTests bool, stats []gocognit.Stat, trace bool) ([]gocognit.Stat, error) { diff --git a/gocognit.go b/gocognit.go index d1f6712..9841bd0 100644 --- a/gocognit.go +++ b/gocognit.go @@ -4,6 +4,7 @@ import ( "fmt" "go/ast" "go/token" + "strconv" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -12,26 +13,58 @@ import ( // Stat is statistic of the complexity. type Stat struct { - PkgName string - FuncName string - Complexity int - Traces []Trace `json:",omitempty"` - Pos token.Position + PkgName string + FuncName string + Complexity int + Diagnostics []Diagnostic `json:",omitempty"` + Pos token.Position } -type Trace struct { +// Diagnostic contrains information how the complexity increase. +type Diagnostic struct { Inc int Nesting int `json:",omitempty"` Text string - Pos token.Position + Pos DiagnosticPosition } -func (t Trace) String() string { - if t.Nesting == 0 { - return fmt.Sprintf("+%d", t.Inc) +// DiagnosticPosition is the position of the diagnostic. +type DiagnosticPosition struct { + Offset int // offset, starting at 0 + Line int // line number, starting at 1 + Column int // column number, starting at 1 (byte count) +} + +func (pos DiagnosticPosition) isValid() bool { + return pos.Line > 0 +} + +func (pos DiagnosticPosition) String() string { + var s string + if pos.isValid() { + if s != "" { + s += ":" + } + + s += strconv.Itoa(pos.Line) + if pos.Column != 0 { + s += fmt.Sprintf(":%d", pos.Column) + } + } + + if s == "" { + s = "-" } - return fmt.Sprintf("+%d (nesting=%d)", t.Inc, t.Nesting) + return s +} + +func (d Diagnostic) String() string { + if d.Nesting == 0 { + return fmt.Sprintf("+%d", d.Inc) + } + + return fmt.Sprintf("+%d (nesting=%d)", d.Inc, d.Nesting) } func (s Stat) String() string { @@ -39,7 +72,12 @@ func (s Stat) String() string { } // ComplexityStats builds the complexity statistics. -func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat, includeTrace bool) []Stat { +func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat) []Stat { + return ComplexityStatsWithDiagnostic(f, fset, stats, false) +} + +// ComplexityStatsWithDiagnostic builds the complexity statistics with diagnostic. +func ComplexityStatsWithDiagnostic(f *ast.File, fset *token.FileSet, stats []Stat, enableDiagnostic bool) []Stat { for _, decl := range f.Decls { if fn, ok := decl.(*ast.FuncDecl); ok { d := parseDirective(fn.Doc) @@ -47,14 +85,14 @@ func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat, includeTrac continue } - res := ScanComplexity(fn, includeTrace) + res := ScanComplexity(fn, enableDiagnostic) stats = append(stats, Stat{ - PkgName: f.Name.Name, - FuncName: funcName(fn), - Complexity: res.Complexity, - Traces: traces(fset, res.Traces), - Pos: fset.Position(fn.Pos()), + PkgName: f.Name.Name, + FuncName: funcName(fn), + Complexity: res.Complexity, + Diagnostics: generateDiagnostics(fset, res.Diagnostics), + Pos: fset.Position(fn.Pos()), }) } } @@ -62,14 +100,21 @@ func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat, includeTrac return stats } -func traces(fset *token.FileSet, traces []trace) []Trace { - tags := make([]Trace, 0, len(traces)) - for _, t := range traces { - tags = append(tags, Trace{ - Inc: t.Inc, - Nesting: t.Nesting, - Text: t.Text, - Pos: fset.Position(t.Pos), +func generateDiagnostics(fset *token.FileSet, diags []diagnostic) []Diagnostic { + tags := make([]Diagnostic, 0, len(diags)) + for _, diag := range diags { + pos := fset.Position(diag.Pos) + tracePos := DiagnosticPosition{ + Offset: pos.Offset, + Line: pos.Line, + Column: pos.Column, + } + + tags = append(tags, Diagnostic{ + Inc: diag.Inc, + Nesting: diag.Nesting, + Text: diag.Text, + Pos: tracePos, }) } @@ -116,26 +161,26 @@ func Complexity(fn *ast.FuncDecl) int { } // ScanComplexity scans the function declaration. -func ScanComplexity(fn *ast.FuncDecl, trace bool) ScanResult { +func ScanComplexity(fn *ast.FuncDecl, includeDiagnostic bool) ScanResult { v := complexityVisitor{ - name: fn.Name, - trace: trace, + name: fn.Name, + diagnosticEnabled: includeDiagnostic, } ast.Walk(&v, fn) return ScanResult{ - Traces: v.traces, - Complexity: v.complexity, + Diagnostics: v.diagnostics, + Complexity: v.complexity, } } type ScanResult struct { - Traces []trace - Complexity int + Diagnostics []diagnostic + Complexity int } -type trace struct { +type diagnostic struct { Inc int Nesting int Text string @@ -151,8 +196,8 @@ type complexityVisitor struct { fset *token.FileSet - trace bool - traces []trace + diagnosticEnabled bool + diagnostics []diagnostic } func (v *complexityVisitor) incNesting() { @@ -166,11 +211,11 @@ func (v *complexityVisitor) decNesting() { func (v *complexityVisitor) incComplexity(text string, pos token.Pos) { v.complexity++ - if !v.trace { + if !v.diagnosticEnabled { return } - v.traces = append(v.traces, trace{ + v.diagnostics = append(v.diagnostics, diagnostic{ Inc: 1, Text: text, Pos: pos, @@ -180,11 +225,11 @@ func (v *complexityVisitor) incComplexity(text string, pos token.Pos) { func (v *complexityVisitor) nestIncComplexity(text string, pos token.Pos) { v.complexity += (v.nesting + 1) - if !v.trace { + if !v.diagnosticEnabled { return } - v.traces = append(v.traces, trace{ + v.diagnostics = append(v.diagnostics, diagnostic{ Inc: v.nesting + 1, Nesting: v.nesting, Text: text, From c2fcfc47f265894eba2a3435f8b83cbc3584e617 Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 07:20:44 +0700 Subject: [PATCH 4/9] Reorder the field position --- cmd/gocognit/main.go | 2 +- gocognit.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/gocognit/main.go b/cmd/gocognit/main.go index 6a18854..d79a13e 100644 --- a/cmd/gocognit/main.go +++ b/cmd/gocognit/main.go @@ -86,8 +86,8 @@ The struct being passed to the template is: PkgName string FuncName string Complexity int - Diagnostics []Diagnostics Pos token.Position + Diagnostics []Diagnostics } type Diagnostic struct { diff --git a/gocognit.go b/gocognit.go index 9841bd0..999b9ba 100644 --- a/gocognit.go +++ b/gocognit.go @@ -16,8 +16,8 @@ type Stat struct { PkgName string FuncName string Complexity int - Diagnostics []Diagnostic `json:",omitempty"` Pos token.Position + Diagnostics []Diagnostic `json:",omitempty"` } // Diagnostic contrains information how the complexity increase. @@ -194,8 +194,6 @@ type complexityVisitor struct { elseNodes map[ast.Node]bool calculatedExprs map[ast.Expr]bool - fset *token.FileSet - diagnosticEnabled bool diagnostics []diagnostic } From f67d1db7964c04e8e7b8c78125d12e2bb7cbbbbd Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 08:35:43 +0700 Subject: [PATCH 5/9] Refine documentation --- README.md | 111 ++++++++++++++++++++++++++++++++++++++----- cmd/gocognit/main.go | 59 +++++++++++++++-------- gocognit.go | 27 ++++++----- 3 files changed, 152 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index f063256..415e81e 100644 --- a/README.md +++ b/README.md @@ -169,14 +169,17 @@ Usage: Flags: - -over N show functions with complexity > N only - and return exit code 1 if the output is non-empty - -top N show the top N most complex functions only - -avg show the average complexity over all functions, - not depending on whether -over or -top are set - -json encode the output as JSON - -f format string the format to use - (default "{{.PkgName}}.{{.FuncName}}:{{.Complexity}}:{{.Pos}}") + -over N show functions with complexity > N only + and return exit code 1 if the output is non-empty + -top N show the top N most complex functions only + -avg show the average complexity over all functions, + not depending on whether -over or -top are set + -test indicates whether test files should be included + -json encode the output as JSON + -d enable diagnostic output + -f format string the format to use + (default "{{.Complexity}} {{.PkgName}} {{.FuncName}} {{.Pos}}") + -ignore expr ignore files matching the given regexp The (default) output fields for each line are: @@ -191,10 +194,24 @@ or equal to The struct being passed to the template is: type Stat struct { - PkgName string - FuncName string - Complexity int - Pos token.Position + PkgName string + FuncName string + Complexity int + Pos token.Position + Diagnostics []Diagnostics + } + + type Diagnostic struct { + Inc string + Nesting int + Text string + Pos DiagnosticPosition + } + + type DiagnosticPosition struct { + Offset int + Line int + Column int } ``` @@ -223,6 +240,76 @@ func IgnoreMe() { } ``` +## Diagnostic +To understand how the complexity are calculated, we can enable the diagnostic by using `-d` flag. + +Example: +```shell +$ gocognit -json -d . +``` + +It will show the diagnostic output in JSON format +
+ +JSON Output + +```json +[ + { + "PkgName": "prime", + "FuncName": "SumOfPrimes", + "Complexity": 7, + "Pos": { + "Filename": "prime.go", + "Offset": 15, + "Line": 3, + "Column": 1 + }, + "Diagnostics": [ + { + "Inc": 1, + "Text": "for", + "Pos": { + "Offset": 69, + "Line": 7, + "Column": 2 + } + }, + { + "Inc": 2, + "Nesting": 1, + "Text": "for", + "Pos": { + "Offset": 104, + "Line": 8, + "Column": 3 + } + }, + { + "Inc": 3, + "Nesting": 2, + "Text": "if", + "Pos": { + "Offset": 152, + "Line": 9, + "Column": 4 + } + }, + { + "Inc": 1, + "Text": "continue", + "Pos": { + "Offset": 190, + "Line": 10, + "Column": 5 + } + } + ] + } +] +``` +
+ ## Related project - [Gocyclo](https://github.com/fzipp/gocyclo) where the code are based on. - [Cognitive Complexity: A new way of measuring understandability](https://www.sonarsource.com/docs/CognitiveComplexity.pdf) white paper by G. Ann Campbell. diff --git a/cmd/gocognit/main.go b/cmd/gocognit/main.go index d79a13e..b617ee6 100644 --- a/cmd/gocognit/main.go +++ b/cmd/gocognit/main.go @@ -10,8 +10,10 @@ // -over N show functions with complexity > N only and return exit code 1 if the output is non-empty // -top N show the top N most complex functions only // -avg show the average complexity over all functions, not depending on whether -over or -top are set +// -test indicates whether test files should be included // -json encode the output as JSON -// -f format string the format to use (default "{{.PkgName}}.{{.FuncName}}:{{.Complexity}}:{{.Pos}}") +// -d enable diagnostic output +// -f format string the format to use (default "{{.Complexity}} {{.PkgName}} {{.FuncName}} {{.Pos}}") // // The (default) output fields for each line are: // @@ -29,8 +31,22 @@ // PkgName string // FuncName string // Complexity int +// Diagnostics []Diagnostic // Pos token.Position // } +// +// type Diagnostic struct { +// Inc string +// Nesting int +// Text string +// Pos DiagnosticPosition +// } +// +// type DiagnosticPosition struct { +// Offset int +// Line int +// Column int +// } package main import ( @@ -59,16 +75,17 @@ Usage: Flags: - -over N show functions with complexity > N only - and return exit code 1 if the output is non-empty - -top N show the top N most complex functions only - -avg show the average complexity over all functions, - not depending on whether -over or -top are set - -test indicates whether test files should be included - -json encode the output as JSON - -d enable diagnostic output - -f format string the format to use - (default "{{.Complexity}} {{.PkgName}} {{.FuncName}} {{.Pos}}") + -over N show functions with complexity > N only + and return exit code 1 if the output is non-empty + -top N show the top N most complex functions only + -avg show the average complexity over all functions, + not depending on whether -over or -top are set + -test indicates whether test files should be included + -json encode the output as JSON + -d enable diagnostic output + -f format string the format to use + (default "{{.Complexity}} {{.PkgName}} {{.FuncName}} {{.Pos}}") + -ignore expr ignore files matching the given regexp The (default) output fields for each line are: @@ -118,14 +135,14 @@ func usage() { func main() { var ( - over int - top int - avg bool - includeTests bool - format string - jsonEncode bool - ignoreExpr string - diagnostic bool + over int + top int + avg bool + includeTests bool + format string + jsonEncode bool + enableDiagnostics bool + ignoreExpr string ) flag.IntVar(&over, "over", defaultOverFlagVal, "show functions with complexity > N only") @@ -134,8 +151,8 @@ func main() { flag.BoolVar(&includeTests, "test", true, "indicates whether test files should be included") flag.StringVar(&format, "f", defaultFormat, "the format to use") flag.BoolVar(&jsonEncode, "json", false, "encode the output as JSON") + flag.BoolVar(&enableDiagnostics, "d", false, "enable diagnostic output") flag.StringVar(&ignoreExpr, "ignore", "", "ignore files matching the given regexp") - flag.BoolVar(&diagnostic, "d", false, "enable diagnostic output") log.SetFlags(0) log.SetPrefix("gocognit: ") @@ -153,7 +170,7 @@ func main() { log.Fatal(err) } - stats, err := analyze(args, includeTests, diagnostic) + stats, err := analyze(args, includeTests, enableDiagnostics) if err != nil { log.Fatal(err) } diff --git a/gocognit.go b/gocognit.go index 999b9ba..74e2c62 100644 --- a/gocognit.go +++ b/gocognit.go @@ -77,7 +77,7 @@ func ComplexityStats(f *ast.File, fset *token.FileSet, stats []Stat) []Stat { } // ComplexityStatsWithDiagnostic builds the complexity statistics with diagnostic. -func ComplexityStatsWithDiagnostic(f *ast.File, fset *token.FileSet, stats []Stat, enableDiagnostic bool) []Stat { +func ComplexityStatsWithDiagnostic(f *ast.File, fset *token.FileSet, stats []Stat, enableDiagnostics bool) []Stat { for _, decl := range f.Decls { if fn, ok := decl.(*ast.FuncDecl); ok { d := parseDirective(fn.Doc) @@ -85,7 +85,7 @@ func ComplexityStatsWithDiagnostic(f *ast.File, fset *token.FileSet, stats []Sta continue } - res := ScanComplexity(fn, enableDiagnostic) + res := ScanComplexity(fn, enableDiagnostics) stats = append(stats, Stat{ PkgName: f.Name.Name, @@ -101,7 +101,8 @@ func ComplexityStatsWithDiagnostic(f *ast.File, fset *token.FileSet, stats []Sta } func generateDiagnostics(fset *token.FileSet, diags []diagnostic) []Diagnostic { - tags := make([]Diagnostic, 0, len(diags)) + out := make([]Diagnostic, 0, len(diags)) + for _, diag := range diags { pos := fset.Position(diag.Pos) tracePos := DiagnosticPosition{ @@ -110,7 +111,7 @@ func generateDiagnostics(fset *token.FileSet, diags []diagnostic) []Diagnostic { Column: pos.Column, } - tags = append(tags, Diagnostic{ + out = append(out, Diagnostic{ Inc: diag.Inc, Nesting: diag.Nesting, Text: diag.Text, @@ -118,7 +119,7 @@ func generateDiagnostics(fset *token.FileSet, diags []diagnostic) []Diagnostic { }) } - return tags + return out } type directive struct { @@ -161,10 +162,10 @@ func Complexity(fn *ast.FuncDecl) int { } // ScanComplexity scans the function declaration. -func ScanComplexity(fn *ast.FuncDecl, includeDiagnostic bool) ScanResult { +func ScanComplexity(fn *ast.FuncDecl, includeDiagnostics bool) ScanResult { v := complexityVisitor{ - name: fn.Name, - diagnosticEnabled: includeDiagnostic, + name: fn.Name, + diagnosticsEnabled: includeDiagnostics, } ast.Walk(&v, fn) @@ -194,8 +195,8 @@ type complexityVisitor struct { elseNodes map[ast.Node]bool calculatedExprs map[ast.Expr]bool - diagnosticEnabled bool - diagnostics []diagnostic + diagnosticsEnabled bool + diagnostics []diagnostic } func (v *complexityVisitor) incNesting() { @@ -209,7 +210,7 @@ func (v *complexityVisitor) decNesting() { func (v *complexityVisitor) incComplexity(text string, pos token.Pos) { v.complexity++ - if !v.diagnosticEnabled { + if !v.diagnosticsEnabled { return } @@ -223,7 +224,7 @@ func (v *complexityVisitor) incComplexity(text string, pos token.Pos) { func (v *complexityVisitor) nestIncComplexity(text string, pos token.Pos) { v.complexity += (v.nesting + 1) - if !v.diagnosticEnabled { + if !v.diagnosticsEnabled { return } @@ -414,6 +415,7 @@ func (v *complexityVisitor) visitFuncLit(n *ast.FuncLit) ast.Visitor { v.incNesting() ast.Walk(v, n.Body) v.decNesting() + return nil } @@ -421,6 +423,7 @@ func (v *complexityVisitor) visitBranchStmt(n *ast.BranchStmt) ast.Visitor { if n.Label != nil { v.incComplexity(n.Tok.String(), n.Pos()) } + return v } From ecddec565f88db866a933335895717447c278873 Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 08:49:23 +0700 Subject: [PATCH 6/9] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- gocognit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gocognit.go b/gocognit.go index 74e2c62..2ed87f3 100644 --- a/gocognit.go +++ b/gocognit.go @@ -20,7 +20,7 @@ type Stat struct { Diagnostics []Diagnostic `json:",omitempty"` } -// Diagnostic contrains information how the complexity increase. +// Diagnostic contains information how the complexity increase. type Diagnostic struct { Inc int Nesting int `json:",omitempty"` From 19f719bb9a72b261189e17f8642722bd055bf06e Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 08:50:02 +0700 Subject: [PATCH 7/9] Update variable name --- gocognit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gocognit.go b/gocognit.go index 2ed87f3..e51ee2a 100644 --- a/gocognit.go +++ b/gocognit.go @@ -105,7 +105,7 @@ func generateDiagnostics(fset *token.FileSet, diags []diagnostic) []Diagnostic { for _, diag := range diags { pos := fset.Position(diag.Pos) - tracePos := DiagnosticPosition{ + diagPos := DiagnosticPosition{ Offset: pos.Offset, Line: pos.Line, Column: pos.Column, @@ -115,7 +115,7 @@ func generateDiagnostics(fset *token.FileSet, diags []diagnostic) []Diagnostic { Inc: diag.Inc, Nesting: diag.Nesting, Text: diag.Text, - Pos: tracePos, + Pos: diagPos, }) } From 26ad1f34c943e45233736d2788f62ec4e4118b6e Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 08:52:26 +0700 Subject: [PATCH 8/9] Add test on Go 1.23 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01c370a..272a420 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: matrix: os: [ubuntu-latest] os-version: ['stable'] - go-version: ['1.19', '1.20', '1.21', '1.22'] + go-version: ['1.19', '1.20', '1.21', '1.22', '1.23'] steps: - name: Checkout From 1f9335edc34c98c3b82e1ed551316129f765eaef Mon Sep 17 00:00:00 2001 From: Nuruddin Ashr Date: Sat, 7 Dec 2024 12:43:41 +0700 Subject: [PATCH 9/9] Add guard on push --- .github/workflows/test.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 272a420..e9eea04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,12 @@ name: Checks + on: - [pull_request] + push: + branches: + - master + pull_request: + branches: + - master jobs: test: