When Go tooling meets broken code

Most writing about Go tooling assumes the input is valid. That is fair: tools usually run on codebases in a healthy state. But edge cases matter. A tool that silently assumes clean input can produce misleading output when the code is wrong. Knowing how Go’s tooling behaves with erroneous code helps you decide what your own tools should do.

Consider a minimal tool that loads a package and dumps its AST:

func processPackage(pkg *packages.Package) {
  for _, fileAst := range pkg.Syntax {
    ast.Print(fset, fileAst)
  }
}

Feed it a file with errors:

package main

func util(x int) {
}

func main() {
  util(5 6)

  var s string = 5.5
}

The tool happily prints an AST. No complaint. At first this surprises people who expect the tool to fail. Look closer at the output and you’ll see it is subtly malformed. The call util(5 6) is syntactically invalid—two arguments without a comma. The parser recovers by keeping the first argument and silently dropping the second:

X: *ast.CallExpr {
.  Fun: *ast.Ident {
.  .  NamePos: sample-module/main.go:9:2
.  .  Name: "util"
.  .  Obj: *(obj @ 23)
.  }
.  Lparen: sample-module/main.go:9:6
.  Args: []ast.Expr (len = 1) {
.  .  0: *ast.BasicLit {
.  .  .  ValuePos: sample-module/main.go:9:7
.  .  .  Kind: INT
.  .  .  Value: "5"
.  .  }
.  }

Why recover at all?

The reason is ergonomics. Many tools back IDEs and editors. If a file has a syntax error mid-way and the tool stopped dead, you would lose syntax highlighting and navigation for everything after that point. Instead, Go tooling makes a best-effort recovery and continues, aiming to give maximum utility even on partially broken code.

That recovery is not guaranteed. Some errors—like a missing closing ) or }—can completely derail the parser.

Reading errors from packages

What if your tool needs to know the code is bad before analyzing the AST? The packages.Package type exposes error information via its Errors field. Add this to processPackage:

if len(pkg.Errors) > 0 {
  fmt.Printf("package %v has %v errors\n", pkg.PkgPath, len(pkg.Errors))

  for _, e := range pkg.Errors {
    var errtype string
    switch e.Kind {
    case packages.ListError:
      errtype = "listing/driver"
    case packages.ParseError:
      errtype = "parser"
    case packages.TypeError:
      errtype = "type checker"
    default:
      errtype = "unknown"
    }
    fmt.Printf("Error [%v]: %s\n", errtype, e)
  }
}

Running it on the erroneous module shows the problems:

package example.com has 3 errors
Error [parser]: sample-module/main.go:9:9: missing ',' in argument list
Error [type checker]: sample-module/main.go:11:17: cannot use 5.5 (untyped float constant) as string value in variable declaration
Error [type checker]: sample-module/main.go:11:6: s declared but not used
<AST dump, if still enabled>

Each entry is a packages.Error. There is a second field, IllTyped, which is only populated when you pass the packages.NeedTypes flag while loading packages:

// IllTyped indicates whether the package or any dependency contains errors.
// It is set only when Types is set.
IllTyped bool

Making the call

What you do with the error report is up to the nature of your tool. If you assume correct input, bail out early when len(pkg.Errors) > 0—producing results from broken code risks phantom output. But if you are building, say, an editor plugin, you may intentionally run as far as possible over partial code and let the user decide.

[1]

Why would there be an error in the middle? Consider that IDEs often repeatedly parse the file, even while we're typing, to be able to provide intellisense features on the fly. While the code is being typed in, it's often un-parsable or has type errors (think of being in the middle of a parameter list fo a function call).

This also highlights an interesting topic in compiler frontends - error recovery. While classical compilers can be forgiven for printing out a few errors and bailing out, tools really do need to recover as quickly as possible to be able to process the rest of the code correctly, even if there's some issue in the middle.