Skip to content

Commit

Permalink
[WIP] Cycle Detection (#17)
Browse files Browse the repository at this point in the history
* basic cycle detection

* add cycle detection issue
  • Loading branch information
notJoon authored Jul 24, 2024
1 parent 7501092 commit d9e4c44
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 3 deletions.
1 change: 1 addition & 0 deletions internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (e *Engine) registerDefaultRules() {
&SimplifySliceExprRule{},
&UnnecessaryConversionRule{},
&LoopAllocationRule{},
&DetectCycleRule{},
)
}

Expand Down
59 changes: 59 additions & 0 deletions internal/lints/detect_cycle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package lints

import (
"go/parser"
"go/token"
"testing"
)

func TestDetectCycle(t *testing.T) {
src := `
package main
type A struct {
B *B
}
type B struct {
A *A
}
var (
x = &y
y = &x
)
func a() {
b()
}
func b() {
a()
}
func outer() {
var inner func()
inner = func() {
outer()
}
inner()
}`

fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
t.Fatalf("failed to parse source: %v", err)
}

cycle := newCycle()
result := cycle.detectCycles(f)
if len(result) != 6 {
// [B A B]
// [B A B]
// [x y x]
// [b a b]
// [outer outer$anon<address> outer]
// [outer outer]
t.Errorf("unexpected result: %v", result)
}
}
171 changes: 171 additions & 0 deletions internal/lints/detect_cycles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package lints

import (
"fmt"
"go/ast"
"go/parser"
"go/token"

tt "github.com/gnoswap-labs/lint/internal/types"
)

func DetectCycle(filename string) ([]tt.Issue, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, err
}

c := newCycle()
cycles := c.detectCycles(node)

var issues []tt.Issue
for _, cycle := range cycles {
issue := tt.Issue{
Rule: "cycle-detection",
Filename: filename,
Start: fset.Position(node.Pos()),
End: fset.Position(node.End()),
Message: "Detected cycle in function call: " + cycle,
}
issues = append(issues, issue)
}
return issues, nil
}

type cycle struct {
dependencies map[string][]string
visited map[string]bool
stack []string
cycles []string
}

func newCycle() *cycle {
return &cycle{
dependencies: make(map[string][]string),
visited: make(map[string]bool),
}
}

func (c *cycle) analyzeFuncDecl(fn *ast.FuncDecl) {
name := fn.Name.Name
c.dependencies[name] = []string{}

ast.Inspect(fn.Body, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
if ident, ok := x.Fun.(*ast.Ident); ok {
c.dependencies[name] = append(c.dependencies[name], ident.Name)
}
case *ast.FuncLit:
c.analyzeFuncLit(x, name)
}
return true
})
}

func (c *cycle) analyzeFuncLit(fn *ast.FuncLit, parentName string) {
anonName := fmt.Sprintf("%s$anon%p", parentName, fn)
c.dependencies[anonName] = []string{}

ast.Inspect(fn.Body, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
if ident, ok := x.Fun.(*ast.Ident); ok {
c.dependencies[anonName] = append(c.dependencies[anonName], ident.Name)
}
case *ast.FuncLit:
c.analyzeFuncLit(x, anonName)
}
return true
})

// add dependency from parent to anonymous function
c.dependencies[parentName] = append(c.dependencies[parentName], anonName)
}

func (c *cycle) detectCycles(node ast.Node) []string {
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
c.analyzeFuncDecl(x)
case *ast.TypeSpec:
c.analyzeTypeSpec(x)
case *ast.ValueSpec:
c.analyzeValueSpec(x)
case *ast.FuncLit:
// handle top-level anonymous functions
c.analyzeFuncLit(x, "topLevel")
}
return true
})

for name := range c.dependencies {
if !c.visited[name] {
c.dfs(name)
}
}

return c.cycles
}

func (c *cycle) analyzeTypeSpec(ts *ast.TypeSpec) {
name := ts.Name.Name
c.dependencies[name] = []string{}

ast.Inspect(ts.Type, func(n ast.Node) bool {
if ident, ok := n.(*ast.Ident); ok {
c.dependencies[name] = append(c.dependencies[name], ident.Name)
}
return true
})
}

func (c *cycle) analyzeValueSpec(vs *ast.ValueSpec) {
for i, name := range vs.Names {
c.dependencies[name.Name] = []string{}
if vs.Values != nil && i < len(vs.Values) {
ast.Inspect(vs.Values[i], func(n ast.Node) bool {
if ident, ok := n.(*ast.Ident); ok {
c.dependencies[name.Name] = append(c.dependencies[name.Name], ident.Name)
}
return true
})
}
}
}

func (c *cycle) dfs(name string) {
c.visited[name] = true
c.stack = append(c.stack, name)

for _, dep := range c.dependencies[name] {
if !c.visited[dep] {
c.dfs(dep)
} else if contains(c.stack, dep) {
cycle := append(c.stack[indexOf(c.stack, dep):], dep)
res := fmt.Sprintf("%v", cycle)
c.cycles = append(c.cycles, res)
}
}

c.stack = c.stack[:len(c.stack)-1]
}

func contains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}

func indexOf(slice []string, item string) int {
for i, v := range slice {
if v == item {
return i
}
}
return -1
}
6 changes: 3 additions & 3 deletions internal/lints/unncessary_type_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func DetectUnnecessaryConversions(filename string) ([]tt.Issue, error) {
if node == n {
return false
}
if contains(node, n) {
if containsNode(node, n) {
parent = node
return false
}
Expand Down Expand Up @@ -207,8 +207,8 @@ func asBuiltin(n ast.Expr, info *types.Info) (*types.Builtin, bool) {
return b, ok
}

// contains checks if parent contains child node
func contains(parent, child ast.Node) bool {
// containsNode checks if parent containsNode child node
func containsNode(parent, child ast.Node) bool {
found := false
ast.Inspect(parent, func(n ast.Node) bool {
if n == child {
Expand Down
6 changes: 6 additions & 0 deletions internal/rule_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,9 @@ type LoopAllocationRule struct{}
func (r *LoopAllocationRule) Check(filename string) ([]tt.Issue, error) {
return lints.DetectLoopAllocation(filename)
}

type DetectCycleRule struct{}

func (r *DetectCycleRule) Check(filename string) ([]tt.Issue, error) {
return lints.DetectCycle(filename)
}
9 changes: 9 additions & 0 deletions testdata/cycle0.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pkg

func a() { // want `a`
b()
}

func b() { // want `b`
a()
}

0 comments on commit d9e4c44

Please sign in to comment.