├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── cmd └── gcassert │ └── main.go ├── gcassert.go ├── gcassert_test.go ├── go.mod ├── go.sum └── testdata ├── bad_directive.go ├── bce.go ├── inline.go ├── issue5.go ├── noescape.go └── otherpkg └── foo.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | gcassert 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Jordan Lewis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcassert 2 | 3 | gcassert is a program for making assertions about compiler decisions in 4 | Go programs, via inline comment directives like `//gcassert:inline`. 5 | 6 | Currently supported [directives](#directives): 7 | 8 | - `//gcassert:inline` to assert function callsites are inlined 9 | - `//gcassert:bce` to assert bounds checks are eliminated 10 | - `//gcassert:noescape` to assert variables don't escape to the heap 11 | 12 | ## Example 13 | 14 | Given a file `foo.go`: 15 | 16 | ```go 17 | package foo 18 | 19 | func addOne(i int) int { 20 | return i+1 21 | } 22 | 23 | //gcassert:inline 24 | func addTwo(i int) int { 25 | return i+1 26 | } 27 | 28 | func a(ints []int) int { 29 | var sum int 30 | for i := range ints { 31 | //gcassert:bce,inline 32 | sum += addOne(ints[i]) 33 | 34 | sum += addTwo(ints[i]) //gcassert:bce 35 | 36 | sum += ints[i] //gcassert:bce 37 | } 38 | return sum 39 | } 40 | ``` 41 | 42 | The inline `//gcassert` directive will cause `gcassert` to fail if the line 43 | `sum += addOne(ints[i])` is either not inlined or contains bounds checks. 44 | 45 | A `//gcassert:inline` directive on a function will cause `gcassert` to fail 46 | if any of the callers of that function do not get inlined. 47 | 48 | `//gcassert` comments expect a comma-separated list of directives after 49 | `//gcassert:`. They can be included above the line in question or after, as an 50 | inline comment. 51 | 52 | ## Installation 53 | 54 | To get the gcassert binary: 55 | 56 | ```bash 57 | go install github.com/jordanlewis/gcassert/cmd/gcassert@latest 58 | ``` 59 | 60 | To get the gcassert library: 61 | 62 | ```bash 63 | go get github.com/jordanlewis/gcassert 64 | ``` 65 | 66 | ## Usage 67 | 68 | ### As a binary 69 | 70 | Run gcassert on packages containing gcassert directives, like this: 71 | 72 | ```bash 73 | gcassert ./package/path 74 | ``` 75 | 76 | The program will output all lines that had a gcassert directive that wasn't 77 | respected by the compiler. 78 | 79 | For example, running on the testdata directory in this library will produce the 80 | following output: 81 | 82 | ```bash 83 | $ gcassert ./testdata 84 | testdata/noescape.go:21: foo := foo{a: 1, b: 2}: foo escapes to heap: 85 | testdata/bce.go:8: fmt.Println(ints[5]): Found IsInBounds 86 | testdata/bce.go:17: sum += notInlinable(ints[i]): call was not inlined 87 | testdata/bce.go:19: sum += notInlinable(ints[i]): call was not inlined 88 | testdata/inline.go:45: alwaysInlined(3): call was not inlined 89 | testdata/inline.go:51: sum += notInlinable(i): call was not inlined 90 | testdata/inline.go:55: sum += 1: call was not inlined 91 | testdata/inline.go:58: test(0).neverInlinedMethod(10): call was not inlined 92 | ``` 93 | 94 | Inspecting each of the listed lines will show a `//gcassert` directive 95 | that wasn't upheld when running the compiler on the package. 96 | 97 | ### As a library 98 | 99 | gcassert is runnable as a library as well, for integration into your linter 100 | suite. It has a single package function, `gcassert.GCAssert`. 101 | 102 | To use it, pass in an `io.Writer` to which errors will be written and a list of 103 | paths to check for `gcassert` assertions, like this: 104 | 105 | ```go 106 | package main 107 | 108 | import "github.com/jordanlewis/gcassert" 109 | 110 | func main() { 111 | var buf strings.Builder 112 | if err := gcassert.GCAssert(&buf, "./path/to/package", "./otherpath/to/package"); err != nil { 113 | // handle non-lint-failure related errors 114 | panic(err) 115 | } 116 | // Output the errors to stdout. 117 | fmt.Println(buf.String()) 118 | } 119 | ``` 120 | 121 | ## Directives 122 | 123 | 124 | ``` 125 | //gcassert:inline 126 | ``` 127 | 128 | The inline directive on a CallExpr asserts that the following statement 129 | contains a function that is inlined by the compiler. If the function does not 130 | get inlined, gcassert will fail. 131 | 132 | The inline directive on a FuncDecl asserts that every caller of that function 133 | is actually inlined by the compiler. 134 | 135 | ``` 136 | //gcassert:bce 137 | ``` 138 | 139 | The bce directive asserts that the following statement contains a slice index 140 | that has no necessary bounds checks. If the compiler adds bounds checks, 141 | gcassert will fail. 142 | 143 | ``` 144 | //gcassert:noescape 145 | ``` 146 | 147 | The noescape directive asserts that the line it's attached to (meaning, 148 | whichever Go AST node is annotated by the comment) produces no "escaped to 149 | heap" messages by the Go compiler. 150 | 151 | The Go compiler emits an "escaped to heap" message for a particular line of 152 | code if any variables on that line of code are forced to escape. 153 | 154 | Typically, the compiler will emit such a message on the line of code that the 155 | variable is declared on. This includes method receivers, method arguments, and 156 | var declarations. 157 | 158 | This means that the annotation must be attached to the line of code that 159 | actually contains the variable in question. For a multi-line function 160 | signature, for example, the annotation must come on the line that has the 161 | variable that would be expected not to escape to the heap: 162 | 163 | ```go 164 | type foo struct { a int } 165 | 166 | // This annotation will pass, because f does not escape. 167 | //gcassert:noescape 168 | func (f foo) returnA( 169 | // This annotation will fail, because a will escape to the heap. 170 | //gcassert:noescape 171 | a int, 172 | ) *int { 173 | return &a 174 | } 175 | ``` 176 | -------------------------------------------------------------------------------- /cmd/gcassert/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/jordanlewis/gcassert" 10 | ) 11 | 12 | func main() { 13 | flag.Parse() 14 | var buf strings.Builder 15 | err := gcassert.GCAssert(&buf, flag.Args()...) 16 | if err != nil { 17 | fmt.Fprintln(os.Stderr, err) 18 | os.Exit(1) 19 | } 20 | output := buf.String() 21 | if len(output) != 0 { 22 | fmt.Fprint(os.Stderr, output) 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gcassert.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "go/ast" 8 | "go/printer" 9 | "go/token" 10 | "go/types" 11 | "io" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "regexp" 16 | "sort" 17 | "strconv" 18 | "strings" 19 | 20 | "golang.org/x/tools/go/packages" 21 | ) 22 | 23 | type assertDirective int 24 | 25 | const ( 26 | noDirective assertDirective = iota 27 | inline 28 | bce 29 | noescape 30 | ) 31 | 32 | func stringToDirective(s string) (assertDirective, error) { 33 | switch s { 34 | case "inline": 35 | return inline, nil 36 | case "bce": 37 | return bce, nil 38 | case "noescape": 39 | return noescape, nil 40 | } 41 | return noDirective, errors.New(fmt.Sprintf("unknown directive %q", s)) 42 | } 43 | 44 | // passInfo contains info on a passed directive for directives that have 45 | // compiler output when they pass, such as the inlining directive. 46 | type passInfo struct { 47 | passed bool 48 | // colNo is the column number of the location of the inlineable callsite. 49 | colNo int 50 | } 51 | 52 | type lineInfo struct { 53 | n ast.Node 54 | directives []assertDirective 55 | 56 | inlinableCallsites []passInfo 57 | // passedDirective is a map from index into the directives slice to a 58 | // boolean that says whether or not the directive succeeded, in the case 59 | // of directives like inlining that have compiler output if they passed. 60 | // For directives like bce that have compiler output if they failed, there's 61 | // no entry in this map. 62 | passedDirective map[int]bool 63 | } 64 | 65 | var gcAssertRegex = regexp.MustCompile(`// ?gcassert:([\w,]+)`) 66 | 67 | type assertVisitor struct { 68 | commentMap ast.CommentMap 69 | 70 | // directiveMap is a map from line number in the source file to the AST node 71 | // that the line number corresponded to, as well as any directives that we 72 | // parsed. 73 | directiveMap map[int]lineInfo 74 | 75 | // mustInlineFuncs is a set of types.Objects that represent FuncDecls of 76 | // some kind that were marked with //gcassert:inline by the user. 77 | mustInlineFuncs map[types.Object]struct{} 78 | fileSet *token.FileSet 79 | cwd string 80 | 81 | p *packages.Package 82 | 83 | errOutput io.Writer 84 | } 85 | 86 | func newAssertVisitor( 87 | commentMap ast.CommentMap, 88 | fileSet *token.FileSet, 89 | cwd string, 90 | p *packages.Package, 91 | mustInlineFuncs map[types.Object]struct{}, 92 | errOutput io.Writer, 93 | ) assertVisitor { 94 | return assertVisitor{ 95 | commentMap: commentMap, 96 | fileSet: fileSet, 97 | cwd: cwd, 98 | directiveMap: make(map[int]lineInfo), 99 | mustInlineFuncs: mustInlineFuncs, 100 | p: p, 101 | errOutput: errOutput, 102 | } 103 | } 104 | 105 | func (v *assertVisitor) Visit(node ast.Node) ast.Visitor { 106 | if node == nil { 107 | return nil 108 | } 109 | pos := v.fileSet.Position(node.Pos()) 110 | 111 | m := v.commentMap[node] 112 | for _, g := range m { 113 | for _, c := range g.List { 114 | matches := gcAssertRegex.FindStringSubmatch(c.Text) 115 | if len(matches) == 0 { 116 | continue 117 | } 118 | // The 0th match is the whole string, and the 1st match is the 119 | // gcassert directive(s). 120 | directiveStrings := strings.Split(matches[1], ",") 121 | 122 | lineInfo := v.directiveMap[pos.Line] 123 | lineInfo.n = node 124 | for _, s := range directiveStrings { 125 | directive, err := stringToDirective(s) 126 | if err != nil { 127 | printAssertionFailure(v.cwd, v.fileSet, node, v.errOutput, err.Error()) 128 | continue 129 | } 130 | if directive == inline { 131 | switch n := node.(type) { 132 | case *ast.FuncDecl: 133 | // Add the Object that this FuncDecl's ident is connected 134 | // to our map of must-inline functions. 135 | obj := v.p.TypesInfo.Defs[n.Name] 136 | if obj != nil { 137 | v.mustInlineFuncs[obj] = struct{}{} 138 | } 139 | continue 140 | } 141 | } 142 | lineInfo.directives = append(lineInfo.directives, directive) 143 | v.directiveMap[pos.Line] = lineInfo 144 | } 145 | } 146 | } 147 | return v 148 | } 149 | 150 | // GCAssert searches through the packages at the input path and writes failures 151 | // to comply with //gcassert directives to the given io.Writer. 152 | func GCAssert(w io.Writer, paths ...string) error { 153 | return GCAssertCwd(w, "", paths...) 154 | } 155 | 156 | // GCAssertCwd performs the same operation as GCAssert, but runs `go build` in 157 | // the provided working directory `cwd`. If `cwd` is the empty string, then 158 | // `go build` will be run in the current working directory. 159 | func GCAssertCwd(w io.Writer, cwd string, paths ...string) error { 160 | var err error 161 | if cwd == "" { 162 | cwd, err = os.Getwd() 163 | if err != nil { 164 | return err 165 | } 166 | } 167 | 168 | fileSet := token.NewFileSet() 169 | pkgs, err := packages.Load(&packages.Config{ 170 | Dir: cwd, 171 | Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedCompiledGoFiles | 172 | packages.NeedTypesInfo | packages.NeedTypes, 173 | Fset: fileSet, 174 | }, paths...) 175 | directiveMap, err := parseDirectives(pkgs, fileSet, cwd, w) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | // Next: invoke Go compiler with -m flags to get the compiler to print 181 | // its optimization decisions. 182 | 183 | args := []string{"build", "-gcflags=-m=2 -d=ssa/check_bce/debug=1"} 184 | for i := range paths { 185 | if filepath.IsAbs(paths[i]) { 186 | args = append(args, paths[i]) 187 | } else { 188 | args = append(args, "./"+paths[i]) 189 | } 190 | } 191 | cmd := exec.Command("go", args...) 192 | cmd.Dir = cwd 193 | pr, pw := io.Pipe() 194 | // Create a temp file to log all diagnostic output. 195 | f, err := os.CreateTemp("", "gcassert-*.log") 196 | if err != nil { 197 | return err 198 | } 199 | fmt.Printf("See %s for full output.\n", f.Name()) 200 | // Log full 'go build' command. 201 | fmt.Fprintln(f, cmd) 202 | mw := io.MultiWriter(pw, f) 203 | cmd.Stdout = mw 204 | cmd.Stderr = mw 205 | cmdErr := make(chan error, 1) 206 | 207 | go func() { 208 | cmdErr <- cmd.Run() 209 | _ = pw.Close() 210 | _ = f.Close() 211 | }() 212 | 213 | scanner := bufio.NewScanner(pr) 214 | optInfo := regexp.MustCompile(`([\.\/\w]+):(\d+):(\d+): (.*)`) 215 | boundsCheck := "Found IsInBounds" 216 | sliceBoundsCheck := "Found IsSliceInBounds" 217 | 218 | for scanner.Scan() { 219 | line := scanner.Text() 220 | matches := optInfo.FindStringSubmatch(line) 221 | if len(matches) != 0 { 222 | path := matches[1] 223 | lineNo, err := strconv.Atoi(matches[2]) 224 | if err != nil { 225 | return err 226 | } 227 | colNo, err := strconv.Atoi(matches[3]) 228 | if err != nil { 229 | return err 230 | } 231 | message := matches[4] 232 | 233 | if !filepath.IsAbs(path) { 234 | path = filepath.Join(cwd, path) 235 | } 236 | if lineToDirectives := directiveMap[path]; lineToDirectives != nil { 237 | info := lineToDirectives[lineNo] 238 | if len(info.directives) > 0 { 239 | if info.passedDirective == nil { 240 | info.passedDirective = make(map[int]bool) 241 | lineToDirectives[lineNo] = info 242 | } 243 | } 244 | for i, d := range info.directives { 245 | switch d { 246 | case bce: 247 | if message == boundsCheck || message == sliceBoundsCheck { 248 | // Error! We found a bounds check where the user expected 249 | // there to be none. 250 | // Print out the user's code lineNo that failed the assertion, 251 | // the assertion itself, and the compiler output that 252 | // proved that the assertion failed. 253 | printAssertionFailure(cwd, fileSet, info.n, w, message) 254 | } 255 | case inline: 256 | if strings.HasPrefix(message, "inlining call to") { 257 | info.passedDirective[i] = true 258 | } 259 | case noescape: 260 | if strings.HasSuffix(message, "escapes to heap:") { 261 | printAssertionFailure(cwd, fileSet, info.n, w, message) 262 | } 263 | } 264 | } 265 | for i := range info.inlinableCallsites { 266 | cs := &info.inlinableCallsites[i] 267 | if cs.colNo == colNo { 268 | cs.passed = true 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | keys := make([]string, 0, len(directiveMap)) 276 | for k := range directiveMap { 277 | keys = append(keys, k) 278 | } 279 | sort.Strings(keys) 280 | 281 | var lines []int 282 | for _, k := range keys { 283 | lines = lines[:0] 284 | lineToDirectives := directiveMap[k] 285 | for line := range lineToDirectives { 286 | lines = append(lines, line) 287 | } 288 | sort.Ints(lines) 289 | for _, line := range lines { 290 | info := lineToDirectives[line] 291 | for _, d := range info.inlinableCallsites { 292 | // An inlining directive passes if it has compiler output. For 293 | // each inlining directive, check if there was matching compiler 294 | // output and fail if not. 295 | if !d.passed { 296 | printAssertionFailure(cwd, fileSet, info.n, w, "call was not inlined") 297 | } 298 | } 299 | for i, d := range info.directives { 300 | if d != inline { 301 | continue 302 | } 303 | if !info.passedDirective[i] { 304 | printAssertionFailure(cwd, fileSet, info.n, w, "call was not inlined") 305 | } 306 | } 307 | } 308 | } 309 | // If 'go build' failed, return the error. 310 | if err := <-cmdErr; err != nil { 311 | return err 312 | } 313 | return nil 314 | } 315 | 316 | func printAssertionFailure(cwd string, fileSet *token.FileSet, n ast.Node, w io.Writer, message string) { 317 | var buf strings.Builder 318 | _ = printer.Fprint(&buf, fileSet, n) 319 | pos := fileSet.Position(n.Pos()) 320 | relPath, err := filepath.Rel(cwd, pos.Filename) 321 | if err != nil { 322 | relPath = pos.Filename 323 | } 324 | fmt.Fprintf(w, "%s:%d:\t%s: %s\n", relPath, pos.Line, buf.String(), message) 325 | } 326 | 327 | // directiveMap maps filepath to line number to lineInfo 328 | type directiveMap map[string]map[int]lineInfo 329 | 330 | func parseDirectives(pkgs []*packages.Package, fileSet *token.FileSet, cwd string, errOutput io.Writer) (directiveMap, error) { 331 | fileDirectiveMap := make(directiveMap) 332 | mustInlineFuncs := make(map[types.Object]struct{}) 333 | for _, pkg := range pkgs { 334 | for i, file := range pkg.Syntax { 335 | commentMap := ast.NewCommentMap(fileSet, file, file.Comments) 336 | 337 | v := newAssertVisitor(commentMap, fileSet, cwd, pkg, mustInlineFuncs, errOutput) 338 | // First: find all lines of code annotated with our gcassert directives. 339 | ast.Walk(&v, file) 340 | 341 | file := pkg.CompiledGoFiles[i] 342 | if len(v.directiveMap) > 0 { 343 | fileDirectiveMap[file] = v.directiveMap 344 | } 345 | } 346 | } 347 | 348 | // Do another pass to find all callsites of funcs marked with inline. 349 | for _, pkg := range pkgs { 350 | for i, file := range pkg.Syntax { 351 | v := &inlinedDeclVisitor{assertVisitor: newAssertVisitor(nil, fileSet, cwd, pkg, mustInlineFuncs, errOutput)} 352 | filePath := pkg.CompiledGoFiles[i] 353 | v.directiveMap = fileDirectiveMap[filePath] 354 | if v.directiveMap == nil { 355 | v.directiveMap = make(map[int]lineInfo) 356 | } 357 | ast.Walk(v, file) 358 | if len(v.directiveMap) > 0 { 359 | fileDirectiveMap[filePath] = v.directiveMap 360 | } 361 | } 362 | } 363 | return fileDirectiveMap, nil 364 | } 365 | 366 | type inlinedDeclVisitor struct { 367 | assertVisitor 368 | } 369 | 370 | func (v *inlinedDeclVisitor) Visit(node ast.Node) ast.Visitor { 371 | if node == nil { 372 | return nil 373 | } 374 | pos := node.Pos() 375 | lineNumber := v.fileSet.Position(pos).Line 376 | 377 | // Search for all func callsites of functions that were marked with 378 | // gcassert:inline and add inline directives to those callsites. 379 | switch n := node.(type) { 380 | case *ast.CallExpr: 381 | callExpr := n 382 | var obj types.Object 383 | switch n := n.Fun.(type) { 384 | case *ast.Ident: 385 | obj = v.p.TypesInfo.Uses[n] 386 | case *ast.SelectorExpr: 387 | sel := v.p.TypesInfo.Selections[n] 388 | if sel != nil { 389 | obj = sel.Obj() 390 | } else { 391 | obj = v.p.TypesInfo.Uses[n.Sel] 392 | } 393 | } 394 | if _, ok := v.mustInlineFuncs[obj]; ok { 395 | lineInfo := v.directiveMap[lineNumber] 396 | lineInfo.n = node 397 | lineInfo.inlinableCallsites = append(lineInfo.inlinableCallsites, 398 | passInfo{colNo: v.fileSet.Position(callExpr.Lparen).Column}) 399 | v.directiveMap[lineNumber] = lineInfo 400 | } 401 | } 402 | return v 403 | } 404 | -------------------------------------------------------------------------------- /gcassert_test.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | import ( 4 | "bytes" 5 | "go/token" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "golang.org/x/tools/go/packages" 13 | ) 14 | 15 | func TestParseDirectives(t *testing.T) { 16 | fileSet := token.NewFileSet() 17 | pkgs, err := packages.Load(&packages.Config{ 18 | Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedCompiledGoFiles | 19 | packages.NeedTypes | packages.NeedTypesInfo, 20 | Fset: fileSet, 21 | }, "./testdata") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | cwd, err := os.Getwd() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | var errOut bytes.Buffer 30 | absMap, err := parseDirectives(pkgs, fileSet, cwd, &errOut) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | assert.Equal(t, `testdata/bad_directive.go:4: //gcassert:foo 35 | func badDirective1() {}: unknown directive "foo" 36 | testdata/bad_directive.go:8: badDirective1(): unknown directive "bar" 37 | testdata/bad_directive.go:12: //gcassert:inline,afterinline 38 | func badDirective3() { 39 | badDirective2() 40 | }: unknown directive "afterinline" 41 | `, errOut.String()) 42 | 43 | // Convert the map into relative paths for ease of testing, and remove 44 | // the syntax node so we don't have to test that as well. 45 | relMap := make(directiveMap, len(absMap)) 46 | for absPath, m := range absMap { 47 | for k, info := range m { 48 | info.n = nil 49 | m[k] = info 50 | } 51 | relPath, err := filepath.Rel(cwd, absPath) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | relMap[relPath] = m 56 | } 57 | 58 | expectedMap := directiveMap{ 59 | "testdata/bad_directive.go": { 60 | 8: {directives: []assertDirective{bce, inline}}, 61 | }, 62 | "testdata/bce.go": { 63 | 8: {directives: []assertDirective{bce}}, 64 | 11: {directives: []assertDirective{bce, inline}}, 65 | 13: {directives: []assertDirective{bce, inline}}, 66 | 17: {directives: []assertDirective{bce, inline}}, 67 | 19: {directives: []assertDirective{bce, inline}}, 68 | 23: {directives: []assertDirective{bce}}, 69 | }, 70 | "testdata/inline.go": { 71 | 45: {inlinableCallsites: []passInfo{{colNo: 15}}}, 72 | 49: {directives: []assertDirective{inline}}, 73 | 51: {directives: []assertDirective{inline}}, 74 | 55: {directives: []assertDirective{inline}}, 75 | 57: {inlinableCallsites: []passInfo{{colNo: 36}}}, 76 | 58: {inlinableCallsites: []passInfo{{colNo: 35}}}, 77 | }, 78 | "testdata/noescape.go": { 79 | 11: {directives: []assertDirective{noescape}}, 80 | 18: {directives: []assertDirective{noescape}}, 81 | 25: {directives: []assertDirective{noescape}}, 82 | 33: {directives: []assertDirective{noescape}}, 83 | 36: {directives: []assertDirective{noescape}}, 84 | }, 85 | "testdata/issue5.go": { 86 | 4: {inlinableCallsites: []passInfo{{colNo: 14}}}, 87 | }, 88 | } 89 | assert.Equal(t, expectedMap, relMap) 90 | } 91 | 92 | func TestGCAssert(t *testing.T) { 93 | cwd, err := os.Getwd() 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | expectedOutput := `testdata/bad_directive.go:4: //gcassert:foo 98 | func badDirective1() {}: unknown directive "foo" 99 | testdata/bad_directive.go:8: badDirective1(): unknown directive "bar" 100 | testdata/bad_directive.go:12: //gcassert:inline,afterinline 101 | func badDirective3() { 102 | badDirective2() 103 | }: unknown directive "afterinline" 104 | testdata/noescape.go:11: foo := foo{a: 1, b: 2}: foo escapes to heap: 105 | testdata/noescape.go:25: // This annotation should fail, because f will escape to the heap. 106 | // 107 | //gcassert:noescape 108 | func (f foo) setA(a int) *foo { 109 | f.a = a 110 | return &f 111 | }: f escapes to heap: 112 | testdata/noescape.go:36: : a escapes to heap: 113 | testdata/bce.go:8: fmt.Println(ints[5]): Found IsInBounds 114 | testdata/bce.go:23: fmt.Println(ints[1:7]): Found IsSliceInBounds 115 | testdata/bce.go:17: sum += notInlinable(ints[i]): call was not inlined 116 | testdata/bce.go:19: sum += notInlinable(ints[i]): call was not inlined 117 | testdata/inline.go:45: alwaysInlined(3): call was not inlined 118 | testdata/inline.go:51: sum += notInlinable(i): call was not inlined 119 | testdata/inline.go:55: sum += 1: call was not inlined 120 | testdata/inline.go:58: test(0).neverInlinedMethod(10): call was not inlined 121 | testdata/inline.go:60: otherpkg.A{}.NeverInlined(sum): call was not inlined 122 | testdata/inline.go:62: otherpkg.NeverInlinedFunc(sum): call was not inlined 123 | testdata/issue5.go:4: Gen().Layout(): call was not inlined 124 | ` 125 | 126 | testCases := []struct{ 127 | name string 128 | pkgs []string 129 | cwd string 130 | expected string 131 | }{ 132 | { 133 | name: "relative", 134 | pkgs: []string{ 135 | "./testdata", 136 | "./testdata/otherpkg", 137 | }, 138 | expected: expectedOutput, 139 | }, 140 | { 141 | name: "absolute", 142 | pkgs: []string{ 143 | filepath.Join(cwd, "testdata"), 144 | filepath.Join(cwd, "testdata/otherpkg"), 145 | }, 146 | expected: expectedOutput, 147 | }, 148 | { 149 | name: "relative-cwd", 150 | pkgs: []string{ 151 | "./testdata", 152 | "./testdata/otherpkg", 153 | }, 154 | cwd: cwd, 155 | expected: expectedOutput, 156 | }, 157 | } 158 | for _, testCase := range testCases { 159 | var w strings.Builder 160 | t.Run(testCase.name, func(t *testing.T) { 161 | var err error 162 | if testCase.cwd == "" { 163 | err = GCAssert(&w, testCase.pkgs...) 164 | } else { 165 | err = GCAssertCwd(&w, testCase.cwd, testCase.pkgs...) 166 | } 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | assert.Equal(t, testCase.expected, w.String()) 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jordanlewis/gcassert 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/stretchr/testify v1.6.1 7 | golang.org/x/tools v0.17.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 11 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 12 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 13 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 15 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 16 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 17 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 18 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 21 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 22 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 23 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 24 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 25 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 30 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 31 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 32 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 36 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 43 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 44 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 45 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 46 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 47 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 48 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 49 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 50 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 51 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 52 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 53 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 54 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 55 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 56 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 57 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 58 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 59 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 60 | golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= 61 | golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 62 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /testdata/bad_directive.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | //gcassert:foo 4 | func badDirective1() {} 5 | 6 | func badDirective2() { 7 | //gcassert:bce,bar,inline 8 | badDirective1() 9 | } 10 | 11 | //gcassert:inline,afterinline 12 | func badDirective3() { 13 | badDirective2() 14 | } 15 | -------------------------------------------------------------------------------- /testdata/bce.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | import "fmt" 4 | 5 | func aLoop(ints []int) int { 6 | sum := 0 7 | //gcassert:bce 8 | fmt.Println(ints[5]) 9 | for i := range ints { 10 | //gcassert:bce,inline 11 | sum += inlinable(ints[i]) 12 | 13 | sum += inlinable(ints[i]) //gcassert:bce,inline 14 | 15 | //gcassert:bce 16 | //gcassert:inline 17 | sum += notInlinable(ints[i]) 18 | 19 | sum += notInlinable(ints[i]) //gcassert:bce,inline 20 | } 21 | // N.B. The statement on line 8 yields 'IsInBounds' check since we can't prove the slice has at least 6 elements. 22 | // Thus, the statement below yields 'IsSliceInBounds' check since we also can't prove it has at least 7 elements. 23 | fmt.Println(ints[1:7]) //gcassert:bce 24 | return sum 25 | } 26 | -------------------------------------------------------------------------------- /testdata/inline.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jordanlewis/gcassert/testdata/otherpkg" 7 | ) 8 | 9 | func inlinable(a int) int { 10 | return a + 2 11 | } 12 | 13 | func notInlinable(a int) int { 14 | for i := 0; i < a; i++ { 15 | fmt.Println(i) 16 | } 17 | return 0 18 | } 19 | 20 | type test int 21 | 22 | //gcassert:inline 23 | func (t test) alwaysInlinedMethod() int { 24 | return 0 25 | } 26 | 27 | //gcassert:inline 28 | func (t test) neverInlinedMethod(n int) int { 29 | sum := 0 30 | for i := 0; i < n; i++ { 31 | fmt.Println(i) 32 | } 33 | return sum 34 | } 35 | 36 | // This assertion makes sure that every callsite to alwaysInlined is in fact 37 | // inlined. 38 | //gcassert:inline 39 | //go:noinline 40 | func alwaysInlined(a int) int { 41 | return a + a 42 | } 43 | 44 | func caller() { 45 | alwaysInlined(3) 46 | sum := 0 47 | for i := 0; i < 10; i++ { 48 | //gcassert:inline 49 | sum += inlinable(i) 50 | //gcassert:inline 51 | sum += notInlinable(i) 52 | } 53 | 54 | // This assertion should fail as there's nothing to inline. 55 | sum += 1 //gcassert:inline 56 | 57 | sum += test(0).alwaysInlinedMethod() 58 | sum += test(0).neverInlinedMethod(10) 59 | 60 | otherpkg.A{}.NeverInlined(sum) 61 | 62 | otherpkg.NeverInlinedFunc(sum) 63 | } 64 | -------------------------------------------------------------------------------- /testdata/issue5.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | func bar() { 4 | Gen().Layout() 5 | } 6 | 7 | func Gen() S { 8 | return S{} 9 | } 10 | 11 | // This assertion should fail, because it's not an inlineable function. This is 12 | // a regression test to assert that it does fail even though the line 13 | // Gen().Layout() has another inlined function in it, Gen(). 14 | 15 | //gcassert:inline 16 | //go:noinline 17 | func (s S) Layout() { 18 | select {} 19 | } 20 | 21 | type S struct{} 22 | -------------------------------------------------------------------------------- /testdata/noescape.go: -------------------------------------------------------------------------------- 1 | package gcassert 2 | 3 | type foo struct { 4 | a int 5 | b int 6 | } 7 | 8 | func returnsStackVarPtr() *foo { 9 | // this should fail 10 | //gcassert:noescape 11 | foo := foo{a: 1, b: 2} 12 | return &foo 13 | } 14 | 15 | func returnsStackVar() foo { 16 | // this should succeed 17 | //gcassert:noescape 18 | foo := foo{a: 1, b: 2} 19 | return foo 20 | } 21 | 22 | // This annotation should fail, because f will escape to the heap. 23 | // 24 | //gcassert:noescape 25 | func (f foo) setA(a int) *foo { 26 | f.a = a 27 | return &f 28 | } 29 | 30 | // This annotation should pass, because f does not escape. 31 | // 32 | //gcassert:noescape 33 | func (f foo) returnA( 34 | // This annotation should fail, because a will escape to the heap. 35 | //gcassert:noescape 36 | a int, 37 | b int, 38 | ) *int { 39 | return &a 40 | } 41 | -------------------------------------------------------------------------------- /testdata/otherpkg/foo.go: -------------------------------------------------------------------------------- 1 | package otherpkg 2 | 3 | import "fmt" 4 | 5 | type A struct{} 6 | 7 | //gcassert:inline 8 | func (a A) NeverInlined(n int) { 9 | for i := 0; i < n; i++ { 10 | fmt.Println(i) 11 | } 12 | } 13 | 14 | //gcassert:inline 15 | func NeverInlinedFunc(n int) { 16 | for i := 0; i < n; i++ { 17 | fmt.Println(i) 18 | } 19 | } 20 | --------------------------------------------------------------------------------