302 lines
12 KiB
Go
302 lines
12 KiB
Go
![]() |
/*
|
|||
|
|
|||
|
Package analysis defines the interface between a modular static
|
|||
|
analysis and an analysis driver program.
|
|||
|
|
|||
|
|
|||
|
Background
|
|||
|
|
|||
|
A static analysis is a function that inspects a package of Go code and
|
|||
|
reports a set of diagnostics (typically mistakes in the code), and
|
|||
|
perhaps produces other results as well, such as suggested refactorings
|
|||
|
or other facts. An analysis that reports mistakes is informally called a
|
|||
|
"checker". For example, the printf checker reports mistakes in
|
|||
|
fmt.Printf format strings.
|
|||
|
|
|||
|
A "modular" analysis is one that inspects one package at a time but can
|
|||
|
save information from a lower-level package and use it when inspecting a
|
|||
|
higher-level package, analogous to separate compilation in a toolchain.
|
|||
|
The printf checker is modular: when it discovers that a function such as
|
|||
|
log.Fatalf delegates to fmt.Printf, it records this fact, and checks
|
|||
|
calls to that function too, including calls made from another package.
|
|||
|
|
|||
|
By implementing a common interface, checkers from a variety of sources
|
|||
|
can be easily selected, incorporated, and reused in a wide range of
|
|||
|
driver programs including command-line tools (such as vet), text editors and
|
|||
|
IDEs, build and test systems (such as go build, Bazel, or Buck), test
|
|||
|
frameworks, code review tools, code-base indexers (such as SourceGraph),
|
|||
|
documentation viewers (such as godoc), batch pipelines for large code
|
|||
|
bases, and so on.
|
|||
|
|
|||
|
|
|||
|
Analyzer
|
|||
|
|
|||
|
The primary type in the API is Analyzer. An Analyzer statically
|
|||
|
describes an analysis function: its name, documentation, flags,
|
|||
|
relationship to other analyzers, and of course, its logic.
|
|||
|
|
|||
|
To define an analysis, a user declares a (logically constant) variable
|
|||
|
of type Analyzer. Here is a typical example from one of the analyzers in
|
|||
|
the go/analysis/passes/ subdirectory:
|
|||
|
|
|||
|
package unusedresult
|
|||
|
|
|||
|
var Analyzer = &analysis.Analyzer{
|
|||
|
Name: "unusedresult",
|
|||
|
Doc: "check for unused results of calls to some functions",
|
|||
|
Run: run,
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
func run(pass *analysis.Pass) (interface{}, error) {
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
An analysis driver is a program such as vet that runs a set of
|
|||
|
analyses and prints the diagnostics that they report.
|
|||
|
The driver program must import the list of Analyzers it needs.
|
|||
|
Typically each Analyzer resides in a separate package.
|
|||
|
To add a new Analyzer to an existing driver, add another item to the list:
|
|||
|
|
|||
|
import ( "unusedresult"; "nilness"; "printf" )
|
|||
|
|
|||
|
var analyses = []*analysis.Analyzer{
|
|||
|
unusedresult.Analyzer,
|
|||
|
nilness.Analyzer,
|
|||
|
printf.Analyzer,
|
|||
|
}
|
|||
|
|
|||
|
A driver may use the name, flags, and documentation to provide on-line
|
|||
|
help that describes the analyses it performs.
|
|||
|
The doc comment contains a brief one-line summary,
|
|||
|
optionally followed by paragraphs of explanation.
|
|||
|
|
|||
|
The Analyzer type has more fields besides those shown above:
|
|||
|
|
|||
|
type Analyzer struct {
|
|||
|
Name string
|
|||
|
Doc string
|
|||
|
Flags flag.FlagSet
|
|||
|
Run func(*Pass) (interface{}, error)
|
|||
|
RunDespiteErrors bool
|
|||
|
ResultType reflect.Type
|
|||
|
Requires []*Analyzer
|
|||
|
FactTypes []Fact
|
|||
|
}
|
|||
|
|
|||
|
The Flags field declares a set of named (global) flag variables that
|
|||
|
control analysis behavior. Unlike vet, analysis flags are not declared
|
|||
|
directly in the command line FlagSet; it is up to the driver to set the
|
|||
|
flag variables. A driver for a single analysis, a, might expose its flag
|
|||
|
f directly on the command line as -f, whereas a driver for multiple
|
|||
|
analyses might prefix the flag name by the analysis name (-a.f) to avoid
|
|||
|
ambiguity. An IDE might expose the flags through a graphical interface,
|
|||
|
and a batch pipeline might configure them from a config file.
|
|||
|
See the "findcall" analyzer for an example of flags in action.
|
|||
|
|
|||
|
The RunDespiteErrors flag indicates whether the analysis is equipped to
|
|||
|
handle ill-typed code. If not, the driver will skip the analysis if
|
|||
|
there were parse or type errors.
|
|||
|
The optional ResultType field specifies the type of the result value
|
|||
|
computed by this analysis and made available to other analyses.
|
|||
|
The Requires field specifies a list of analyses upon which
|
|||
|
this one depends and whose results it may access, and it constrains the
|
|||
|
order in which a driver may run analyses.
|
|||
|
The FactTypes field is discussed in the section on Modularity.
|
|||
|
The analysis package provides a Validate function to perform basic
|
|||
|
sanity checks on an Analyzer, such as that its Requires graph is
|
|||
|
acyclic, its fact and result types are unique, and so on.
|
|||
|
|
|||
|
Finally, the Run field contains a function to be called by the driver to
|
|||
|
execute the analysis on a single package. The driver passes it an
|
|||
|
instance of the Pass type.
|
|||
|
|
|||
|
|
|||
|
Pass
|
|||
|
|
|||
|
A Pass describes a single unit of work: the application of a particular
|
|||
|
Analyzer to a particular package of Go code.
|
|||
|
The Pass provides information to the Analyzer's Run function about the
|
|||
|
package being analyzed, and provides operations to the Run function for
|
|||
|
reporting diagnostics and other information back to the driver.
|
|||
|
|
|||
|
type Pass struct {
|
|||
|
Fset *token.FileSet
|
|||
|
Files []*ast.File
|
|||
|
OtherFiles []string
|
|||
|
Pkg *types.Package
|
|||
|
TypesInfo *types.Info
|
|||
|
ResultOf map[*Analyzer]interface{}
|
|||
|
Report func(Diagnostic)
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
The Fset, Files, Pkg, and TypesInfo fields provide the syntax trees,
|
|||
|
type information, and source positions for a single package of Go code.
|
|||
|
|
|||
|
The OtherFiles field provides the names, but not the contents, of non-Go
|
|||
|
files such as assembly that are part of this package. See the "asmdecl"
|
|||
|
or "buildtags" analyzers for examples of loading non-Go files and reporting
|
|||
|
diagnostics against them.
|
|||
|
|
|||
|
The ResultOf field provides the results computed by the analyzers
|
|||
|
required by this one, as expressed in its Analyzer.Requires field. The
|
|||
|
driver runs the required analyzers first and makes their results
|
|||
|
available in this map. Each Analyzer must return a value of the type
|
|||
|
described in its Analyzer.ResultType field.
|
|||
|
For example, the "ctrlflow" analyzer returns a *ctrlflow.CFGs, which
|
|||
|
provides a control-flow graph for each function in the package (see
|
|||
|
golang.org/x/tools/go/cfg); the "inspect" analyzer returns a value that
|
|||
|
enables other Analyzers to traverse the syntax trees of the package more
|
|||
|
efficiently; and the "buildssa" analyzer constructs an SSA-form
|
|||
|
intermediate representation.
|
|||
|
Each of these Analyzers extends the capabilities of later Analyzers
|
|||
|
without adding a dependency to the core API, so an analysis tool pays
|
|||
|
only for the extensions it needs.
|
|||
|
|
|||
|
The Report function emits a diagnostic, a message associated with a
|
|||
|
source position. For most analyses, diagnostics are their primary
|
|||
|
result.
|
|||
|
For convenience, Pass provides a helper method, Reportf, to report a new
|
|||
|
diagnostic by formatting a string.
|
|||
|
Diagnostic is defined as:
|
|||
|
|
|||
|
type Diagnostic struct {
|
|||
|
Pos token.Pos
|
|||
|
Category string // optional
|
|||
|
Message string
|
|||
|
}
|
|||
|
|
|||
|
The optional Category field is a short identifier that classifies the
|
|||
|
kind of message when an analysis produces several kinds of diagnostic.
|
|||
|
|
|||
|
Most Analyzers inspect typed Go syntax trees, but a few, such as asmdecl
|
|||
|
and buildtag, inspect the raw text of Go source files or even non-Go
|
|||
|
files such as assembly. To report a diagnostic against a line of a
|
|||
|
raw text file, use the following sequence:
|
|||
|
|
|||
|
content, err := ioutil.ReadFile(filename)
|
|||
|
if err != nil { ... }
|
|||
|
tf := fset.AddFile(filename, -1, len(content))
|
|||
|
tf.SetLinesForContent(content)
|
|||
|
...
|
|||
|
pass.Reportf(tf.LineStart(line), "oops")
|
|||
|
|
|||
|
|
|||
|
Modular analysis with Facts
|
|||
|
|
|||
|
To improve efficiency and scalability, large programs are routinely
|
|||
|
built using separate compilation: units of the program are compiled
|
|||
|
separately, and recompiled only when one of their dependencies changes;
|
|||
|
independent modules may be compiled in parallel. The same technique may
|
|||
|
be applied to static analyses, for the same benefits. Such analyses are
|
|||
|
described as "modular".
|
|||
|
|
|||
|
A compiler’s type checker is an example of a modular static analysis.
|
|||
|
Many other checkers we would like to apply to Go programs can be
|
|||
|
understood as alternative or non-standard type systems. For example,
|
|||
|
vet's printf checker infers whether a function has the "printf wrapper"
|
|||
|
type, and it applies stricter checks to calls of such functions. In
|
|||
|
addition, it records which functions are printf wrappers for use by
|
|||
|
later analysis passes to identify other printf wrappers by induction.
|
|||
|
A result such as “f is a printf wrapper” that is not interesting by
|
|||
|
itself but serves as a stepping stone to an interesting result (such as
|
|||
|
a diagnostic) is called a "fact".
|
|||
|
|
|||
|
The analysis API allows an analysis to define new types of facts, to
|
|||
|
associate facts of these types with objects (named entities) declared
|
|||
|
within the current package, or with the package as a whole, and to query
|
|||
|
for an existing fact of a given type associated with an object or
|
|||
|
package.
|
|||
|
|
|||
|
An Analyzer that uses facts must declare their types:
|
|||
|
|
|||
|
var Analyzer = &analysis.Analyzer{
|
|||
|
Name: "printf",
|
|||
|
FactTypes: []analysis.Fact{new(isWrapper)},
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
type isWrapper struct{} // => *types.Func f “is a printf wrapper”
|
|||
|
|
|||
|
The driver program ensures that facts for a pass’s dependencies are
|
|||
|
generated before analyzing the package and is responsible for propagating
|
|||
|
facts from one package to another, possibly across address spaces.
|
|||
|
Consequently, Facts must be serializable. The API requires that drivers
|
|||
|
use the gob encoding, an efficient, robust, self-describing binary
|
|||
|
protocol. A fact type may implement the GobEncoder/GobDecoder interfaces
|
|||
|
if the default encoding is unsuitable. Facts should be stateless.
|
|||
|
|
|||
|
The Pass type has functions to import and export facts,
|
|||
|
associated either with an object or with a package:
|
|||
|
|
|||
|
type Pass struct {
|
|||
|
...
|
|||
|
ExportObjectFact func(types.Object, Fact)
|
|||
|
ImportObjectFact func(types.Object, Fact) bool
|
|||
|
|
|||
|
ExportPackageFact func(fact Fact)
|
|||
|
ImportPackageFact func(*types.Package, Fact) bool
|
|||
|
}
|
|||
|
|
|||
|
An Analyzer may only export facts associated with the current package or
|
|||
|
its objects, though it may import facts from any package or object that
|
|||
|
is an import dependency of the current package.
|
|||
|
|
|||
|
Conceptually, ExportObjectFact(obj, fact) inserts fact into a hidden map keyed by
|
|||
|
the pair (obj, TypeOf(fact)), and the ImportObjectFact function
|
|||
|
retrieves the entry from this map and copies its value into the variable
|
|||
|
pointed to by fact. This scheme assumes that the concrete type of fact
|
|||
|
is a pointer; this assumption is checked by the Validate function.
|
|||
|
See the "printf" analyzer for an example of object facts in action.
|
|||
|
|
|||
|
Some driver implementations (such as those based on Bazel and Blaze) do
|
|||
|
not currently apply analyzers to packages of the standard library.
|
|||
|
Therefore, for best results, analyzer authors should not rely on
|
|||
|
analysis facts being available for standard packages.
|
|||
|
For example, although the printf checker is capable of deducing during
|
|||
|
analysis of the log package that log.Printf is a printf wrapper,
|
|||
|
this fact is built in to the analyzer so that it correctly checks
|
|||
|
calls to log.Printf even when run in a driver that does not apply
|
|||
|
it to standard packages. We would like to remove this limitation in future.
|
|||
|
|
|||
|
|
|||
|
Testing an Analyzer
|
|||
|
|
|||
|
The analysistest subpackage provides utilities for testing an Analyzer.
|
|||
|
In a few lines of code, it is possible to run an analyzer on a package
|
|||
|
of testdata files and check that it reported all the expected
|
|||
|
diagnostics and facts (and no more). Expectations are expressed using
|
|||
|
"// want ..." comments in the input code.
|
|||
|
|
|||
|
|
|||
|
Standalone commands
|
|||
|
|
|||
|
Analyzers are provided in the form of packages that a driver program is
|
|||
|
expected to import. The vet command imports a set of several analyzers,
|
|||
|
but users may wish to define their own analysis commands that perform
|
|||
|
additional checks. To simplify the task of creating an analysis command,
|
|||
|
either for a single analyzer or for a whole suite, we provide the
|
|||
|
singlechecker and multichecker subpackages.
|
|||
|
|
|||
|
The singlechecker package provides the main function for a command that
|
|||
|
runs one analyzer. By convention, each analyzer such as
|
|||
|
go/passes/findcall should be accompanied by a singlechecker-based
|
|||
|
command such as go/analysis/passes/findcall/cmd/findcall, defined in its
|
|||
|
entirety as:
|
|||
|
|
|||
|
package main
|
|||
|
|
|||
|
import (
|
|||
|
"golang.org/x/tools/go/analysis/passes/findcall"
|
|||
|
"golang.org/x/tools/go/analysis/singlechecker"
|
|||
|
)
|
|||
|
|
|||
|
func main() { singlechecker.Main(findcall.Analyzer) }
|
|||
|
|
|||
|
A tool that provides multiple analyzers can use multichecker in a
|
|||
|
similar way, giving it the list of Analyzers.
|
|||
|
|
|||
|
*/
|
|||
|
package analysis
|