Skip to content
Merged
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ Find structurally duplicate functions in Go code.
godedup ./...
```

**HTML output**

![HTML](https://raw.githubusercontent.com/hashmap-kz/assets/main/godedup/godedup-html-v1.png)

**Table output with clickable `file.go:line` locations in supported terminals and editors**

```
$ godedup --output=table --exclude '_test\.go$'

Expand Down Expand Up @@ -83,14 +89,15 @@ Examples:
godedup --exclude '_test\.go$' --exclude '\.pb\.go$' ./...
godedup --exclude '(_test|[.]pb|[.]deepcopy)[.]go$' ./...
godedup --output table ./...
godedup --output html ./... > godedup.html
godedup --output json ./... | jq .

Flags:
--min-similarity float minimum similarity threshold (default: 0.85)
--min-stmts int minimum statements to analyze (default: 3)
--exact report only exact structural clones
--exclude exclude files matching regexp (may be repeated)
--output string output format: text, table, json (default: text)
--output string output format: text, table, html, json (default: text)
--version print version
```

Expand Down
1 change: 1 addition & 0 deletions internal/hash/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type FuncInfo struct {
StmtSeq []uint64 // per-statement hashes for similarity comparison
NumStmts int // total statement count (excluding blank lines)
NumLines int // line span of the function body
Source string // original function source, used by rich reports
}

// Hasher computes structural hashes of AST nodes.
Expand Down
18 changes: 17 additions & 1 deletion internal/load/load.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package load

import (
"bytes"
"go/ast"
"go/parser"
"go/token"
Expand Down Expand Up @@ -79,7 +80,12 @@ func parseFile(path string, fset *token.FileSet, hasher *hash.Hasher, inp *cmd.L
return nil
}

f, err := parser.ParseFile(fset, path, nil, 0)
src, err := os.ReadFile(path)
if err != nil {
return err
}

f, err := parser.ParseFile(fset, path, src, 0)
if err != nil {
// skip unparseable files
return nil
Expand All @@ -99,6 +105,7 @@ func parseFile(path string, fset *token.FileSet, hasher *hash.Hasher, inp *cmd.L
}

info := hasher.HashFunc(pkg, path, fn)
info.Source = sourceSpan(src, fset, fn)
if info.Name == "" {
continue
}
Expand All @@ -107,3 +114,12 @@ func parseFile(path string, fset *token.FileSet, hasher *hash.Hasher, inp *cmd.L

return nil
}

func sourceSpan(src []byte, fset *token.FileSet, fn *ast.FuncDecl) string {
start := fset.Position(fn.Pos()).Offset
end := fset.Position(fn.End()).Offset
if start < 0 || end < start || end > len(src) {
return ""
}
return string(bytes.TrimRight(src[start:end], "\n"))
}
24 changes: 24 additions & 0 deletions internal/load/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/hashmap-kz/godedup/internal/cmd"
Expand Down Expand Up @@ -164,6 +165,29 @@ func Vendored() int {
}
}

func TestLoadStoresFunctionSource(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "source.go")
writeFile(t, file, `package sample
func Source() int {
a := 1
b := 2
return a + b
}
`)

result, err := Load([]string{file}, emptyLoadInput())
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(result.Funcs) != 1 {
t.Fatalf("len(Funcs) = %d, want 1", len(result.Funcs))
}
if got := result.Funcs[0].Source; !strings.Contains(got, "func Source() int") || !strings.Contains(got, "return a + b") {
t.Fatalf("Source was not captured correctly:\n%s", got)
}
}

func TestLoadSingleFile(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "single.go")
Expand Down
7 changes: 7 additions & 0 deletions internal/report/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package report

const (
kindExact = "EXACT"
kindNear = "NEAR"
sim100Percent = "100%"
)
Loading