Skip to content

Commit 5fc0ab4

Browse files
committed
🚀
1 parent cfebf16 commit 5fc0ab4

File tree

6 files changed

+87
-40
lines changed

6 files changed

+87
-40
lines changed

‎README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# breakcheck
22

3-
breackcheck checks exported values, types and function declarations in your working tree for potential breaking changes against a given git reference.
3+
breakcheck checks exported values, types and function declarations in your working tree for potential breaking changes against a given git reference.
44

55
## Usage
66

@@ -23,19 +23,32 @@ Flags:
2323
Below is an example output from running breakcheck against the datadog-agent repository:
2424

2525
```
26-
$ breakcheck --base=head~40
26+
$ breakcheck --base=HEAD~40
2727
pkg/util/clusteragent:
28-
• removed: struct DCAClient, field ClusterAgentAPIEndpoint
29-
• changed: struct DCAClient, field ClusterAgentVersion type from string to version.Version
30-
• before: func GetClusterAgentClient() (*DCAClient, error)
31-
now: func GetClusterAgentClient() (DCAClientInterface, error)
32-
(return value 0 changed from *DCAClient to DCAClientInterface)
33-
34-
pkg/logs/config:
35-
• removed: const ContainerdType
28+
29+
• Removed struct field "ClusterAgentAPIEndpoint":
30+
- clusteragent.go:42@HEAD~40:
31+
struct DCAClient
32+
- clusteragent.go:57:
33+
struct DCAClient
34+
35+
• Struct field "ClusterAgentVersion" type changed from string to version.Version:
36+
- clusteragent.go:42@HEAD~40:
37+
DCAClient
38+
39+
• Return value (0) changed from *DCAClient to DCAClientInterface:
40+
- clusteragent.go:60@HEAD~40:
41+
func GetClusterAgentClient() (*DCAClient, error)
42+
- clusteragent.go:75:
43+
func GetClusterAgentClient() (DCAClientInterface, error)
3644
```
3745

3846
## Caveats
3947

4048
* If a function's argument is changed to an alias of the same type, breakcheck will fail to detect this and will report it as a change. Technically this is not a breaking change.
4149
* Detecting changes in exported package level value declarations is limited to their name and type (when known).
50+
51+
## Similar work
52+
53+
* https://golang.org/x/tools/internal/apidiff
54+
* https://github.com/bradleyfalzon/apicompat

‎comparer.go

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ package main
33
import (
44
"fmt"
55
"go/ast"
6+
"go/token"
67
"strings"
78
)
89

9-
func newDeclComparer(summary *packageSummary) *declComparer {
10+
func newDeclComparer(fset *token.FileSet, path string, summary *packageSummary) *declComparer {
1011
return &declComparer{
12+
fset: fset,
13+
path: path,
1114
head: summary,
1215
}
1316
}
1417

1518
// declComparer is an ast.Visitor which ensures that all encountered
1619
// declarations have been unmodified in the provided summary.
1720
type declComparer struct {
21+
fset *token.FileSet
22+
path string // relative folder
1823
head *packageSummary
1924
report strings.Builder
2025
}
@@ -47,20 +52,26 @@ func (c *declComparer) Visit(node ast.Node) ast.Visitor {
4752
func (c *declComparer) compareType(base *ast.TypeSpec) {
4853
head, ok := c.head.types[base.Name.Name]
4954
if !ok {
50-
c.logf("• removed: type %s", base.Name.Name)
55+
c.logf("\n• Removed in working tree:")
56+
c.logPosition(base, true)
57+
c.logf(" type %s", base.Name.Name)
5158
return
5259
}
5360
if x, ok := base.Type.(*ast.StructType); ok {
5461
// compare structs, allow adding new fields
5562
y, ok := head.Type.(*ast.StructType)
5663
if !ok {
57-
c.logf("• changed: struct %s type changed to %s", base.Name.Name, describeType(head.Type))
64+
c.logf("\n• Type changed to %s:", describeType(head.Type))
65+
c.logPosition(base, true)
66+
c.logf(" type %s", base.Name.Name)
5867
}
5968
c.compareStructs(base.Name.Name, x, y)
6069
return
6170
}
6271
if a, b := describeType(base.Type), describeType(head.Type); a != b {
63-
c.logf("• changed: type %s from %s to %s", base.Name.Name, a, b)
72+
c.logf("\n• Type changed from %s to %s:", a, b)
73+
c.logPosition(base, true)
74+
c.logf(" type %s", base.Name.Name)
6475
}
6576
}
6677

@@ -70,13 +81,19 @@ func (c *declComparer) compareStructs(structName string, base, head *ast.StructT
7081
for _, n := range f.Names {
7182
if n.Name == name {
7283
if a, b := describeType(f.Type), describeType(typ); a != b {
73-
c.logf("• changed: struct %s, field %s type from %s to %s", structName, name, b, a)
84+
c.logf("\n• Struct field %q type changed from %s to %s:", name, b, a)
85+
c.logPosition(base, true)
86+
c.logf(" %s", structName)
7487
}
7588
return
7689
}
7790
}
7891
}
79-
c.logf("• removed: struct %s, field %s", structName, name)
92+
c.logf("\n• Removed struct field %q:", name)
93+
c.logPosition(base, true)
94+
c.logf(" struct %s", structName)
95+
c.logPosition(head, false)
96+
c.logf(" struct %s", structName)
8097
}
8198
for _, field := range base.Fields.List {
8299
for _, name := range field.Names {
@@ -89,11 +106,15 @@ func (c *declComparer) compareValue(base *ast.ValueSpec) {
89106
for _, name := range base.Names {
90107
head, ok := c.head.value[name.Name]
91108
if !ok {
92-
c.logf("• removed: %s", printValue(base))
109+
c.logf("\n• Value removed in working tree:")
110+
c.logPosition(base, true)
111+
c.logf(" %s", printValue(base))
93112
return
94113
}
95114
if a, b := describeType(head.Type), describeType(base.Type); a != b {
96-
c.logf("• changed: value type %s from %s to %s", name.Name, b, a)
115+
c.logf("\n• Value type changed from %s to %s in working tree:", b, a)
116+
c.logPosition(base, true)
117+
c.logf(" %s", name.Name)
97118
}
98119
if base.Type == nil {
99120
// If the type is nil, this is likely an assignment where the type is inferred
@@ -111,7 +132,9 @@ func (c *declComparer) compareFunc(base *ast.FuncDecl) {
111132
head, ok := c.head.funcs[name]
112133
if !ok {
113134
// func not found
114-
c.logf("• removed: %s", printFunc(base.Recv, base.Name, base.Type))
135+
c.logf("\n• Func removed:")
136+
c.logPosition(base, true)
137+
c.logf(" %s", printFunc(base.Recv, base.Name, base.Type))
115138
return
116139
}
117140
headArgs := head.Type.Params.List
@@ -120,46 +143,61 @@ func (c *declComparer) compareFunc(base *ast.FuncDecl) {
120143
// if there's only one new argument in base, and that argument
121144
// is variadic, then this isn't a breaking change
122145
if diff != 1 || !strings.HasPrefix(describeType(headArgs[len(headArgs)-1].Type), "...") {
123-
c.logFuncChange(base, head, "change in argument count")
146+
c.logFuncChange(base, head, "Change in argument count")
124147
return
125148
}
126149
}
127150
for i, arg := range baseArgs {
128151
if a, b := describeType(arg.Type), describeType(headArgs[i].Type); a != b {
129-
c.logFuncChange(base, head, fmt.Sprintf("argument %d changed from %s to %s", i, a, b))
152+
c.logFuncChange(base, head, fmt.Sprintf("Argument (%d) changed from %s to %s", i, a, b))
130153
return
131154
}
132155
}
133156
baseResults := base.Type.Results
134157
headResults := head.Type.Results
135158
if baseResults == nil && headResults != nil {
136-
c.logFuncChange(base, head, "return values were added")
159+
c.logFuncChange(base, head, "Return values were added")
137160
return
138161
}
139162
if baseResults != nil && headResults == nil {
140-
c.logFuncChange(base, head, "return values were removed")
163+
c.logFuncChange(base, head, "Return values were removed")
141164
return
142165
}
143166
if baseResults == nil && headResults == nil {
144167
// OK
145168
return
146169
}
147170
if len(baseResults.List) != len(headResults.List) {
148-
c.logFuncChange(base, head, "change in return value count")
171+
c.logFuncChange(base, head, "Change in return value count")
149172
return
150173
}
151174
for i, arg := range baseResults.List {
152175
if a, b := describeType(arg.Type), describeType(headResults.List[i].Type); a != b {
153-
c.logFuncChange(base, head, fmt.Sprintf("return value %d changed from %s to %s", i, a, b))
176+
c.logFuncChange(base, head, fmt.Sprintf("Return value (%d) changed from %s to %s", i, a, b))
154177
return
155178
}
156179
}
157180
}
158181

182+
func (c *declComparer) logPosition(node ast.Node, base bool) {
183+
fset := c.fset
184+
path := c.path
185+
gitr := "@" + *baseRef
186+
if !base {
187+
fset = c.head.fset
188+
path = c.head.path
189+
gitr = ""
190+
}
191+
pos := fset.Position(node.Pos())
192+
c.logf(" - %s:%d%s:", strings.TrimPrefix(pos.Filename, path+"/"), pos.Line, gitr)
193+
}
194+
159195
func (c *declComparer) logFuncChange(base, head *ast.FuncDecl, reason string) {
160-
c.logf("• before: %s", printFunc(base.Recv, base.Name, base.Type))
161-
c.logf(" now: %s", printFunc(head.Recv, head.Name, head.Type))
162-
c.logf(" (%s)", reason)
196+
c.logf("\n• %s:", reason)
197+
c.logPosition(base, true)
198+
c.logf(" %s", printFunc(base.Recv, base.Name, base.Type))
199+
c.logPosition(head, false)
200+
c.logf(" %s", printFunc(head.Recv, head.Name, head.Type))
163201
}
164202

165203
func (c *declComparer) logf(format string, args ...interface{}) {
@@ -168,7 +206,7 @@ func (c *declComparer) logf(format string, args ...interface{}) {
168206
c.report.WriteByte(':')
169207
c.report.WriteByte('\n')
170208
}
171-
c.report.WriteByte('\t')
209+
c.report.WriteString(" ")
172210
c.report.WriteString(fmt.Sprintf(format, args...))
173211
c.report.WriteByte('\n')
174212
}

‎go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
module github.com/gbbr/breakcheck
22

33
go 1.12
4-
5-
require github.com/kr/pretty v0.1.0

‎go.sum

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
2-
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
3-
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
4-
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
5-
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=

‎main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
var (
16-
baseRef = flag.String("base", "head", "git reference to compare against")
16+
baseRef = flag.String("base", "HEAD", "git reference to compare against")
1717
verbose = flag.Bool("v", false, "enable verbose mode")
1818
privRecv = flag.Bool("private", false, "include exported methods with private receivers")
1919
)
@@ -82,7 +82,7 @@ func main() {
8282
}
8383

8484
// create working path summary
85-
summary := newPackageSummary(dir)
85+
summary := newPackageSummary(fsetHead, dir)
8686
for _, d := range list {
8787
if d.IsDir() || !strings.HasSuffix(d.Name(), ".go") || strings.HasSuffix(d.Name(), "_test.go") {
8888
continue
@@ -99,7 +99,7 @@ func main() {
9999
}
100100

101101
// scan base, everything found there should exist in working path
102-
comparer := newDeclComparer(summary)
102+
comparer := newDeclComparer(fsetBase, dir, summary)
103103
blobs, err := gitLsTreeGoBlobs(*baseRef, dir)
104104
if err != nil {
105105
log.Fatal(err)

‎summary.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ package main
22

33
import (
44
"go/ast"
5+
"go/token"
56
"strings"
67
)
78

89
type packageSummary struct {
10+
fset *token.FileSet
911
path string // relative folder
1012
funcs map[string]*ast.FuncDecl // maps (recv.)name to func
1113
types map[string]*ast.TypeSpec // maps type name to spec
1214
value map[string]*ast.ValueSpec // maps value name to spec
1315
// TODO: add package name, ensure it wasn't changed
1416
}
1517

16-
func newPackageSummary(path string) *packageSummary {
18+
func newPackageSummary(fset *token.FileSet, path string) *packageSummary {
1719
return &packageSummary{
20+
fset: fset,
1821
path: path,
1922
funcs: make(map[string]*ast.FuncDecl),
2023
value: make(map[string]*ast.ValueSpec),

0 commit comments

Comments
 (0)