yoo, so last week i was surfing through gopls - trying to reduce its heap usage so that my laptop becomes somewhat usable. in fact in the process of defining what processes to turn off (including some static analyzers), i went a bit further into what analyzers are in go standard libraries, and how do analyzers work.
the unusedresult analyzer
so i chose one - unusedresult analyzer for my learning. this is the static analyzer that gives you build time warnings when the result of a defined method isn't used. interestingly, this warning doesn't appear when we ignore return values of a random function, in fact only a few standard methods (like Sprintf) are hard-coded into this analyzer implementation [1].
what it does is simply, traverse the AST preorder (abstract syntax tree) which gopls creates using go/parser and go/token, determines if node is a method, matches the callee to defined methods that need to be checked, get the callee info if return variable lies on the tree. if it doesn't, it simply means the return value isn't used anywhere, and go ahead with printing a warning.
why build one myself
static analyzers are great, and that got me into thinking why not build one myself? interestingly, a couple of weeks back we came across a piece of code that was potentially vulnerable to SQL injection attacks since the SQL query construction involved fmt.Sprintf directly and not parameterized construction (?) which gorm then sanitizes. that led me to thinking that this could easily go unwatched by a human reviewer, and an AI reviewer could easily be blown away if abstraction or change lines become bigger than it could handle. so this was a perfect use case for building a static analyzer that checks this.
in fact this could be extended pretty neatly to other use cases, especially handling potential security vulnerabilities.
how it works
the approach is straightforward once you understand how go/analysis works -
- register an analyzer using
analysis.Analyzerwith an inspect-type requirement - the analyzer receives the AST via
inspect.Preorder- which visits every node top-down - for each
*ast.CallExprnode, resolve the callee function - check if the callee is
fmt.Sprintf(or other formatting functions) - walk up the AST to see if this call is an argument to a database method (like
db.Raw(),db.Exec(),db.Where()) - if it is - flag it as a potential SQL injection vulnerability, suggest using parameterized queries instead
the key insight from studying unusedresult was that you don't need to do complex data flow analysis for many practical checks - just pattern matching on the AST is enough.
what's next
this pattern of walking the AST and matching against known-dangerous patterns is surprisingly powerful. thinking of extending this to catch -
- hardcoded secrets or API keys in source
- unsafe type assertions without comma-ok
- unbounded goroutine spawning in loops
the go/analysis framework makes it really easy to plug these into existing tooling - gopls, golangci-lint, or even CI pipelines. honestly, the hardest part wasn't writing the analyzer, it was understanding how gopls stitches everything together.
[1] https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/unusedresult/unusedresult.go