whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
*.golden -text
|
||||
*.svg binary
|
||||
**/testdata/** -text
|
||||
@@ -0,0 +1,2 @@
|
||||
patreon: dominikh
|
||||
github: dominikh
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: 💢 False positive in Staticcheck
|
||||
about: Your code is fine but Staticcheck complains about it, anyway.
|
||||
labels: false-positive, needs-triage
|
||||
title: ""
|
||||
---
|
||||
<!--
|
||||
Please make sure to include the following information in your issue report:
|
||||
|
||||
- The output of 'staticcheck -version'
|
||||
- The output of 'staticcheck -debug.version' (it is fine if this command fails)
|
||||
- The output of 'go version'
|
||||
- The output of 'go env'
|
||||
- Exactly which command you ran
|
||||
- Output of the command and what's wrong with the output
|
||||
- Where we can read the code you're running Staticcheck on
|
||||
(GitHub repo, link to playground, code embedded in the issue, ...)
|
||||
-->
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: 🦆 False negative in Staticcheck
|
||||
about: Your code is wrong but Staticcheck doesn't complain about it.
|
||||
labels: false-negative, needs-triage
|
||||
title: ""
|
||||
---
|
||||
<!--
|
||||
Please make sure to include the following information in your issue report:
|
||||
|
||||
- The output of 'staticcheck -version'
|
||||
- The output of 'staticcheck -debug.version' (it is fine if this command fails)
|
||||
- The output of 'go version'
|
||||
- The output of 'go env'
|
||||
- Exactly which command you ran
|
||||
- Output of the command and what's wrong with the output
|
||||
- Where we can read the code you're running Staticcheck on
|
||||
(GitHub repo, link to playground, code embedded in the issue, ...)
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: 🐞 General bugs with Staticcheck
|
||||
about: Something in Staticcheck isn't working as it should.
|
||||
labels: bug, needs-triage
|
||||
title: ""
|
||||
---
|
||||
<!--
|
||||
Please make sure to include the following information in your issue report:
|
||||
|
||||
- The output of 'staticcheck -version'
|
||||
- The output of 'staticcheck -debug.version' (it is fine if this command fails)
|
||||
- The output of 'go version'
|
||||
- The output of 'go env'
|
||||
- Exactly which command you ran
|
||||
- Output of the command and what's wrong with the output
|
||||
- Where we can read the code you're running Staticcheck on
|
||||
(GitHub repo, link to playground, code embedded in the issue, ...)
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: 🛠 Other
|
||||
about: Ideas, feature requests, and all other issues not fitting into another category.
|
||||
labels: needs-triage
|
||||
title: ""
|
||||
---
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,56 @@
|
||||
name: "CI"
|
||||
on: ["push", "pull_request"]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: "Run CI"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.19.x"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: WillAbides/setup-go-faster@v1.7.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- run: "go test ./..."
|
||||
- run: "go vet ./..."
|
||||
- uses: dominikh/staticcheck-action@9f77055cca7bfaafb836cbd6720865187f5fbf51
|
||||
with:
|
||||
version: "3d6c86f0908ab82d6ff280219e2968ee65f83b2e"
|
||||
min-go-version: "1.17"
|
||||
install-go: false
|
||||
cache-key: ${{ matrix.go }}
|
||||
output-format: binary
|
||||
output-file: "./staticcheck.bin"
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "staticcheck-${{ github.sha }}-${{ matrix.go }}-${{ matrix.os }}.bin"
|
||||
path: "./staticcheck.bin"
|
||||
retention-days: 1
|
||||
if-no-files-found: warn
|
||||
output:
|
||||
name: "Output Staticcheck findings"
|
||||
needs: ci
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: WillAbides/setup-go-faster@v1.7.0
|
||||
with:
|
||||
go-version: "1.19.x"
|
||||
# this downloads all artifacts of the current workflow into the current working directory, creating one directory per artifact
|
||||
- uses: actions/download-artifact@v3
|
||||
- id: glob
|
||||
run: |
|
||||
# We replace newliens with %0A, which GitHub apparently magically turns back into newlines
|
||||
out=$(ls -1 ./staticcheck-*.bin/*.bin)
|
||||
echo "::set-output name=files::${out//$'\n'/%0A}"
|
||||
- uses: dominikh/staticcheck-action@9f77055cca7bfaafb836cbd6720865187f5fbf51
|
||||
with:
|
||||
version: "3d6c86f0908ab82d6ff280219e2968ee65f83b2e"
|
||||
min-go-version: "1.19"
|
||||
install-go: false
|
||||
merge-files: ${{ steps.glob.outputs.files }}
|
||||
@@ -0,0 +1,14 @@
|
||||
/cmd/keyify/keyify
|
||||
/cmd/staticcheck/staticcheck
|
||||
/cmd/structlayout-optimize/structlayout-optimize
|
||||
/cmd/structlayout-pretty/structlayout-pretty
|
||||
/cmd/structlayout/structlayout
|
||||
/dist/20??.?.?/
|
||||
/dist/20??.?/
|
||||
/internal/cmd/irdump/irdump
|
||||
/website/.hugo_build.lock
|
||||
/website/public
|
||||
/website/resources
|
||||
/website/assets/img/sponsors
|
||||
/website/data/sponsors.toml
|
||||
/website/data/copyrights.toml
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "website/themes/docsy"]
|
||||
path = website/themes/docsy
|
||||
url = https://github.com/google/docsy
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2016 Dominik Honnef
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,121 @@
|
||||
Staticcheck and its related tools make use of third party projects,
|
||||
either by reusing their code, or by statically linking them into
|
||||
resulting binaries. These projects are:
|
||||
|
||||
* The Go Programming Language - https://golang.org/
|
||||
golang.org/x/mod - https://github.com/golang/mod
|
||||
golang.org/x/tools - https://github.com/golang/tools
|
||||
golang.org/x/sys - https://github.com/golang/sys
|
||||
golang.org/x/xerrors - https://github.com/golang/xerrors
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
* github.com/BurntSushi/toml - https://github.com/BurntSushi/toml
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
|
||||
* gogrep - https://github.com/mvdan/gogrep
|
||||
|
||||
Copyright (c) 2017, Daniel Martí. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
* gosmith - https://github.com/dvyukov/gosmith
|
||||
|
||||
Copyright (c) 2014 Dmitry Vyukov. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* The name of Dmitry Vyukov may be used to endorse or promote
|
||||
products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,63 @@
|
||||
<div align="center">
|
||||
<h1><img alt="Staticcheck logo" src="/images/logo.svg" height="300" /><br />
|
||||
The advanced Go linter
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
Staticcheck is a state of the art linter for the [Go programming
|
||||
language](https://go.dev/). Using static analysis, it finds bugs and performance issues,
|
||||
offers simplifications, and enforces style rules.
|
||||
|
||||
**Financial support by [private and corporate sponsors](http://staticcheck.io/sponsors) guarantees the tool's continued development.
|
||||
Please [become a sponsor](https://github.com/users/dominikh/sponsorship) if you or your company rely on Staticcheck.**
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find extensive documentation on Staticcheck on [its website](https://staticcheck.io/docs/).
|
||||
|
||||
## Installation
|
||||
|
||||
### Releases
|
||||
|
||||
It is recommended that you run released versions of the tools.
|
||||
These releases can be found as git tags (e.g. `2022.1`).
|
||||
|
||||
The easiest way of installing a release is by using `go install`, for example `go install honnef.co/go/tools/cmd/staticcheck@2022.1`.
|
||||
Alternatively, we also offer [prebuilt binaries](https://github.com/dominikh/go-tools/releases).
|
||||
|
||||
You can find more information about installation and releases in the [documentation](https://staticcheck.io/docs/getting-started/).
|
||||
|
||||
### Master
|
||||
|
||||
You can also run the master branch instead of a release. Note that
|
||||
while the master branch is usually stable, it may still contain new
|
||||
checks or backwards incompatible changes that break your build. By
|
||||
using the master branch you agree to become a beta tester.
|
||||
|
||||
## Tools
|
||||
|
||||
All of the following tools can be found in the cmd/ directory. Each
|
||||
tool is accompanied by its own README, describing it in more detail.
|
||||
|
||||
| Tool | Description |
|
||||
|----------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| [keyify](cmd/keyify/) | Transforms an unkeyed struct literal into a keyed one. |
|
||||
| [staticcheck](cmd/staticcheck/) | Go static analysis, detecting bugs, performance issues, and much more. |
|
||||
| [structlayout](cmd/structlayout/) | Displays the layout (field sizes and padding) of structs. |
|
||||
| [structlayout-optimize](cmd/structlayout-optimize) | Reorders struct fields to minimize the amount of padding. |
|
||||
| [structlayout-pretty](cmd/structlayout-pretty) | Formats the output of structlayout with ASCII art. |
|
||||
|
||||
## Libraries
|
||||
|
||||
In addition to the aforementioned tools, this repository contains the
|
||||
libraries necessary to implement these tools.
|
||||
|
||||
Unless otherwise noted, none of these libraries have stable APIs.
|
||||
Their main purpose is to aid the implementation of the tools.
|
||||
You'll have to expect semiregular backwards-incompatible changes if you decide to use these libraries.
|
||||
|
||||
## System requirements
|
||||
|
||||
Releases support the current and previous version of Go at the time of release.
|
||||
The master branch supports the current version of Go.
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
declare -A PKGS=(
|
||||
["strconv"]="strconv"
|
||||
["net/http"]="net/http"
|
||||
["image/color"]="image/color"
|
||||
["std"]="std"
|
||||
["k8s"]="k8s.io/kubernetes/pkg/..."
|
||||
)
|
||||
|
||||
MIN_CORES=32
|
||||
MAX_CORES=32
|
||||
INCR_CORES=2
|
||||
MIN_GOGC=100
|
||||
MAX_GOGC=100
|
||||
SAMPLES=10
|
||||
WIPE_CACHE=1
|
||||
FORMAT=bench
|
||||
BIN=$(realpath ./silent-staticcheck.sh)
|
||||
|
||||
runBenchmark() {
|
||||
local pkg="$1"
|
||||
local label="$2"
|
||||
local gc="$3"
|
||||
local cores="$4"
|
||||
local wipe="$5"
|
||||
|
||||
if [ $wipe -ne 0 ]; then
|
||||
rm -rf ~/.cache/staticcheck
|
||||
fi
|
||||
|
||||
local out=$(GOGC=$gc GOMAXPROCS=$cores env time -f "%e %M" $BIN $pkg 2>&1)
|
||||
local t=$(echo "$out" | cut -f1 -d" ")
|
||||
local m=$(echo "$out" | cut -f2 -d" ")
|
||||
local ns=$(printf "%s 1000000000 * p" $t | dc)
|
||||
local b=$((m * 1024))
|
||||
|
||||
case $FORMAT in
|
||||
bench)
|
||||
printf "BenchmarkStaticcheck-%s-GOGC%d-wiped%d-%d 1 %.0f ns/op %.0f B/op\n" "$label" "$gc" "$wipe" "$cores" "$ns" "$b"
|
||||
;;
|
||||
csv)
|
||||
printf "%s,%d,%d,%d,%.0f,%.0f\n" "$label" "$gc" "$cores" "$wipe" "$ns" "$b"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
export GO111MODULE=off
|
||||
|
||||
if [ "$FORMAT" = "csv" ]; then
|
||||
printf "packages,gogc,gomaxprocs,wipe-cache,time,memory\n"
|
||||
fi
|
||||
|
||||
for label in "${!PKGS[@]}"; do
|
||||
pkg=${PKGS[$label]}
|
||||
for gc in $(seq $MIN_GOGC 10 $MAX_GOGC); do
|
||||
for cores in $(seq $MIN_CORES $INCR_CORES $MAX_CORES); do
|
||||
for i in $(seq 1 $SAMPLES); do
|
||||
runBenchmark "$pkg" "$label" "$gc" "$cores" 1
|
||||
runBenchmark "$pkg" "$label" "$gc" "$cores" 0
|
||||
done
|
||||
done
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
/home/dominikh/prj/src/honnef.co/go/tools/cmd/staticcheck/staticcheck -checks "all" -fail "" $1 &>/dev/null
|
||||
exit 0
|
||||
@@ -0,0 +1,357 @@
|
||||
// Package code answers structural and type questions about Go code.
|
||||
package code
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/analysis/facts/generated"
|
||||
"honnef.co/go/tools/analysis/facts/purity"
|
||||
"honnef.co/go/tools/analysis/facts/tokenfile"
|
||||
"honnef.co/go/tools/go/ast/astutil"
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
"honnef.co/go/tools/pattern"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
type Positioner interface {
|
||||
Pos() token.Pos
|
||||
}
|
||||
|
||||
func IsOfType(pass *analysis.Pass, expr ast.Expr, name string) bool {
|
||||
return typeutil.IsType(pass.TypesInfo.TypeOf(expr), name)
|
||||
}
|
||||
|
||||
func IsInTest(pass *analysis.Pass, node Positioner) bool {
|
||||
// FIXME(dh): this doesn't work for global variables with
|
||||
// initializers
|
||||
f := pass.Fset.File(node.Pos())
|
||||
return f != nil && strings.HasSuffix(f.Name(), "_test.go")
|
||||
}
|
||||
|
||||
// IsMain reports whether the package being processed is a package
|
||||
// main.
|
||||
func IsMain(pass *analysis.Pass) bool {
|
||||
return pass.Pkg.Name() == "main"
|
||||
}
|
||||
|
||||
// IsMainLike reports whether the package being processed is a
|
||||
// main-like package. A main-like package is a package that is
|
||||
// package main, or that is intended to be used by a tool framework
|
||||
// such as cobra to implement a command.
|
||||
//
|
||||
// Note that this function errs on the side of false positives; it may
|
||||
// return true for packages that aren't main-like. IsMainLike is
|
||||
// intended for analyses that wish to suppress diagnostics for
|
||||
// main-like packages to avoid false positives.
|
||||
func IsMainLike(pass *analysis.Pass) bool {
|
||||
if pass.Pkg.Name() == "main" {
|
||||
return true
|
||||
}
|
||||
for _, imp := range pass.Pkg.Imports() {
|
||||
if imp.Path() == "github.com/spf13/cobra" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SelectorName(pass *analysis.Pass, expr *ast.SelectorExpr) string {
|
||||
info := pass.TypesInfo
|
||||
sel := info.Selections[expr]
|
||||
if sel == nil {
|
||||
if x, ok := expr.X.(*ast.Ident); ok {
|
||||
pkg, ok := info.ObjectOf(x).(*types.PkgName)
|
||||
if !ok {
|
||||
// This shouldn't happen
|
||||
return fmt.Sprintf("%s.%s", x.Name, expr.Sel.Name)
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", pkg.Imported().Path(), expr.Sel.Name)
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported selector: %v", expr))
|
||||
}
|
||||
if v, ok := sel.Obj().(*types.Var); ok && v.IsField() {
|
||||
return fmt.Sprintf("(%s).%s", typeutil.DereferenceR(sel.Recv()), sel.Obj().Name())
|
||||
} else {
|
||||
return fmt.Sprintf("(%s).%s", sel.Recv(), sel.Obj().Name())
|
||||
}
|
||||
}
|
||||
|
||||
func IsNil(pass *analysis.Pass, expr ast.Expr) bool {
|
||||
return pass.TypesInfo.Types[expr].IsNil()
|
||||
}
|
||||
|
||||
func BoolConst(pass *analysis.Pass, expr ast.Expr) bool {
|
||||
val := pass.TypesInfo.ObjectOf(expr.(*ast.Ident)).(*types.Const).Val()
|
||||
return constant.BoolVal(val)
|
||||
}
|
||||
|
||||
func IsBoolConst(pass *analysis.Pass, expr ast.Expr) bool {
|
||||
// We explicitly don't support typed bools because more often than
|
||||
// not, custom bool types are used as binary enums and the
|
||||
// explicit comparison is desired.
|
||||
|
||||
ident, ok := expr.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
obj := pass.TypesInfo.ObjectOf(ident)
|
||||
c, ok := obj.(*types.Const)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
basic, ok := c.Type().(*types.Basic)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if basic.Kind() != types.UntypedBool && basic.Kind() != types.Bool {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ExprToInt(pass *analysis.Pass, expr ast.Expr) (int64, bool) {
|
||||
tv := pass.TypesInfo.Types[expr]
|
||||
if tv.Value == nil {
|
||||
return 0, false
|
||||
}
|
||||
if tv.Value.Kind() != constant.Int {
|
||||
return 0, false
|
||||
}
|
||||
return constant.Int64Val(tv.Value)
|
||||
}
|
||||
|
||||
func ExprToString(pass *analysis.Pass, expr ast.Expr) (string, bool) {
|
||||
val := pass.TypesInfo.Types[expr].Value
|
||||
if val == nil {
|
||||
return "", false
|
||||
}
|
||||
if val.Kind() != constant.String {
|
||||
return "", false
|
||||
}
|
||||
return constant.StringVal(val), true
|
||||
}
|
||||
|
||||
func CallName(pass *analysis.Pass, call *ast.CallExpr) string {
|
||||
fun := astutil.Unparen(call.Fun)
|
||||
|
||||
// Instantiating a function cannot return another generic function, so doing this once is enough
|
||||
switch idx := fun.(type) {
|
||||
case *ast.IndexExpr:
|
||||
fun = idx.X
|
||||
case *ast.IndexListExpr:
|
||||
fun = idx.X
|
||||
}
|
||||
|
||||
// (foo)[T] is not a valid instantiationg, so no need to unparen again.
|
||||
|
||||
switch fun := fun.(type) {
|
||||
case *ast.SelectorExpr:
|
||||
fn, ok := pass.TypesInfo.ObjectOf(fun.Sel).(*types.Func)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return typeutil.FuncName(fn)
|
||||
case *ast.Ident:
|
||||
obj := pass.TypesInfo.ObjectOf(fun)
|
||||
switch obj := obj.(type) {
|
||||
case *types.Func:
|
||||
return typeutil.FuncName(obj)
|
||||
case *types.Builtin:
|
||||
return obj.Name()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func IsCallTo(pass *analysis.Pass, node ast.Node, name string) bool {
|
||||
call, ok := node.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return CallName(pass, call) == name
|
||||
}
|
||||
|
||||
func IsCallToAny(pass *analysis.Pass, node ast.Node, names ...string) bool {
|
||||
call, ok := node.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
q := CallName(pass, call)
|
||||
for _, name := range names {
|
||||
if q == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func File(pass *analysis.Pass, node Positioner) *ast.File {
|
||||
m := pass.ResultOf[tokenfile.Analyzer].(map[*token.File]*ast.File)
|
||||
return m[pass.Fset.File(node.Pos())]
|
||||
}
|
||||
|
||||
// IsGenerated reports whether pos is in a generated file, It ignores
|
||||
// //line directives.
|
||||
func IsGenerated(pass *analysis.Pass, pos token.Pos) bool {
|
||||
_, ok := Generator(pass, pos)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Generator returns the generator that generated the file containing
|
||||
// pos. It ignores //line directives.
|
||||
func Generator(pass *analysis.Pass, pos token.Pos) (generated.Generator, bool) {
|
||||
file := pass.Fset.PositionFor(pos, false).Filename
|
||||
m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator)
|
||||
g, ok := m[file]
|
||||
return g, ok
|
||||
}
|
||||
|
||||
// MayHaveSideEffects reports whether expr may have side effects. If
|
||||
// the purity argument is nil, this function implements a purely
|
||||
// syntactic check, meaning that any function call may have side
|
||||
// effects, regardless of the called function's body. Otherwise,
|
||||
// purity will be consulted to determine the purity of function calls.
|
||||
func MayHaveSideEffects(pass *analysis.Pass, expr ast.Expr, purity purity.Result) bool {
|
||||
switch expr := expr.(type) {
|
||||
case *ast.BadExpr:
|
||||
return true
|
||||
case *ast.Ellipsis:
|
||||
return MayHaveSideEffects(pass, expr.Elt, purity)
|
||||
case *ast.FuncLit:
|
||||
// the literal itself cannot have side effects, only calling it
|
||||
// might, which is handled by CallExpr.
|
||||
return false
|
||||
case *ast.ArrayType, *ast.StructType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.ChanType:
|
||||
// types cannot have side effects
|
||||
return false
|
||||
case *ast.BasicLit:
|
||||
return false
|
||||
case *ast.BinaryExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity) || MayHaveSideEffects(pass, expr.Y, purity)
|
||||
case *ast.CallExpr:
|
||||
if purity == nil {
|
||||
return true
|
||||
}
|
||||
switch obj := typeutil.Callee(pass.TypesInfo, expr).(type) {
|
||||
case *types.Func:
|
||||
if _, ok := purity[obj]; !ok {
|
||||
return true
|
||||
}
|
||||
case *types.Builtin:
|
||||
switch obj.Name() {
|
||||
case "len", "cap":
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return true
|
||||
}
|
||||
for _, arg := range expr.Args {
|
||||
if MayHaveSideEffects(pass, arg, purity) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case *ast.CompositeLit:
|
||||
if MayHaveSideEffects(pass, expr.Type, purity) {
|
||||
return true
|
||||
}
|
||||
for _, elt := range expr.Elts {
|
||||
if MayHaveSideEffects(pass, elt, purity) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case *ast.Ident:
|
||||
return false
|
||||
case *ast.IndexExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity) || MayHaveSideEffects(pass, expr.Index, purity)
|
||||
case *ast.IndexListExpr:
|
||||
// In theory, none of the checks are necessary, as IndexListExpr only involves types. But there is no harm in
|
||||
// being safe.
|
||||
if MayHaveSideEffects(pass, expr.X, purity) {
|
||||
return true
|
||||
}
|
||||
for _, idx := range expr.Indices {
|
||||
if MayHaveSideEffects(pass, idx, purity) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case *ast.KeyValueExpr:
|
||||
return MayHaveSideEffects(pass, expr.Key, purity) || MayHaveSideEffects(pass, expr.Value, purity)
|
||||
case *ast.SelectorExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity)
|
||||
case *ast.SliceExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity) ||
|
||||
MayHaveSideEffects(pass, expr.Low, purity) ||
|
||||
MayHaveSideEffects(pass, expr.High, purity) ||
|
||||
MayHaveSideEffects(pass, expr.Max, purity)
|
||||
case *ast.StarExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity)
|
||||
case *ast.TypeAssertExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity)
|
||||
case *ast.UnaryExpr:
|
||||
if MayHaveSideEffects(pass, expr.X, purity) {
|
||||
return true
|
||||
}
|
||||
return expr.Op == token.ARROW || expr.Op == token.AND
|
||||
case *ast.ParenExpr:
|
||||
return MayHaveSideEffects(pass, expr.X, purity)
|
||||
case nil:
|
||||
return false
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled type %T", expr))
|
||||
}
|
||||
}
|
||||
|
||||
func IsGoVersion(pass *analysis.Pass, minor int) bool {
|
||||
f, ok := pass.Analyzer.Flags.Lookup("go").Value.(flag.Getter)
|
||||
if !ok {
|
||||
panic("requested Go version, but analyzer has no version flag")
|
||||
}
|
||||
version := f.Get().(int)
|
||||
return version >= minor
|
||||
}
|
||||
|
||||
var integerLiteralQ = pattern.MustParse(`(IntegerLiteral tv)`)
|
||||
|
||||
func IntegerLiteral(pass *analysis.Pass, node ast.Node) (types.TypeAndValue, bool) {
|
||||
m, ok := Match(pass, integerLiteralQ, node)
|
||||
if !ok {
|
||||
return types.TypeAndValue{}, false
|
||||
}
|
||||
return m.State["tv"].(types.TypeAndValue), true
|
||||
}
|
||||
|
||||
func IsIntegerLiteral(pass *analysis.Pass, node ast.Node, value constant.Value) bool {
|
||||
tv, ok := IntegerLiteral(pass, node)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return constant.Compare(tv.Value, token.EQL, value)
|
||||
}
|
||||
|
||||
// IsMethod reports whether expr is a method call of a named method with signature meth.
|
||||
// If name is empty, it is not checked.
|
||||
// For now, method expressions (Type.Method(recv, ..)) are not considered method calls.
|
||||
func IsMethod(pass *analysis.Pass, expr *ast.SelectorExpr, name string, meth *types.Signature) bool {
|
||||
if name != "" && expr.Sel.Name != name {
|
||||
return false
|
||||
}
|
||||
sel, ok := pass.TypesInfo.Selections[expr]
|
||||
if !ok || sel.Kind() != types.MethodVal {
|
||||
return false
|
||||
}
|
||||
return types.Identical(sel.Type(), meth)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
|
||||
"honnef.co/go/tools/pattern"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/go/analysis/passes/inspect"
|
||||
"golang.org/x/tools/go/ast/inspector"
|
||||
)
|
||||
|
||||
func Preorder(pass *analysis.Pass, fn func(ast.Node), types ...ast.Node) {
|
||||
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder(types, fn)
|
||||
}
|
||||
|
||||
func PreorderStack(pass *analysis.Pass, fn func(ast.Node, []ast.Node), types ...ast.Node) {
|
||||
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).WithStack(types, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
|
||||
if push {
|
||||
fn(n, stack)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func Match(pass *analysis.Pass, q pattern.Pattern, node ast.Node) (*pattern.Matcher, bool) {
|
||||
// Note that we ignore q.Relevant – callers of Match usually use
|
||||
// AST inspectors that already filter on nodes we're interested
|
||||
// in.
|
||||
m := &pattern.Matcher{TypesInfo: pass.TypesInfo}
|
||||
ok := m.Match(q, node)
|
||||
return m, ok
|
||||
}
|
||||
|
||||
func MatchAndEdit(pass *analysis.Pass, before, after pattern.Pattern, node ast.Node) (*pattern.Matcher, []analysis.TextEdit, bool) {
|
||||
m, ok := Match(pass, before, node)
|
||||
if !ok {
|
||||
return m, nil, false
|
||||
}
|
||||
r := pattern.NodeToAST(after.Root, m.State)
|
||||
buf := &bytes.Buffer{}
|
||||
format.Node(buf, pass.Fset, r)
|
||||
edit := []analysis.TextEdit{{
|
||||
Pos: node.Pos(),
|
||||
End: node.End(),
|
||||
NewText: buf.Bytes(),
|
||||
}}
|
||||
return m, edit, true
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Package edit contains helpers for creating suggested fixes.
|
||||
package edit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/token"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"honnef.co/go/tools/pattern"
|
||||
)
|
||||
|
||||
// Ranger describes values that have a start and end position.
|
||||
// In most cases these are either ast.Node or manually constructed ranges.
|
||||
type Ranger interface {
|
||||
Pos() token.Pos
|
||||
End() token.Pos
|
||||
}
|
||||
|
||||
// Range implements the Ranger interface.
|
||||
type Range [2]token.Pos
|
||||
|
||||
func (r Range) Pos() token.Pos { return r[0] }
|
||||
func (r Range) End() token.Pos { return r[1] }
|
||||
|
||||
// ReplaceWithString replaces a range with a string.
|
||||
func ReplaceWithString(old Ranger, new string) analysis.TextEdit {
|
||||
return analysis.TextEdit{
|
||||
Pos: old.Pos(),
|
||||
End: old.End(),
|
||||
NewText: []byte(new),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceWithNode replaces a range with an AST node.
|
||||
func ReplaceWithNode(fset *token.FileSet, old Ranger, new ast.Node) analysis.TextEdit {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := format.Node(buf, fset, new); err != nil {
|
||||
panic("internal error: " + err.Error())
|
||||
}
|
||||
return analysis.TextEdit{
|
||||
Pos: old.Pos(),
|
||||
End: old.End(),
|
||||
NewText: buf.Bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceWithPattern replaces a range with the result of executing a pattern.
|
||||
func ReplaceWithPattern(fset *token.FileSet, old Ranger, new pattern.Pattern, state pattern.State) analysis.TextEdit {
|
||||
r := pattern.NodeToAST(new.Root, state)
|
||||
buf := &bytes.Buffer{}
|
||||
format.Node(buf, fset, r)
|
||||
return analysis.TextEdit{
|
||||
Pos: old.Pos(),
|
||||
End: old.End(),
|
||||
NewText: buf.Bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes a range of code.
|
||||
func Delete(old Ranger) analysis.TextEdit {
|
||||
return analysis.TextEdit{
|
||||
Pos: old.Pos(),
|
||||
End: old.End(),
|
||||
NewText: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func Fix(msg string, edits ...analysis.TextEdit) analysis.SuggestedFix {
|
||||
return analysis.SuggestedFix{
|
||||
Message: msg,
|
||||
TextEdits: edits,
|
||||
}
|
||||
}
|
||||
|
||||
// Selector creates a new selector expression.
|
||||
func Selector(x, sel string) *ast.SelectorExpr {
|
||||
return &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: x},
|
||||
Sel: &ast.Ident{Name: sel},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package deprecated
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
type IsDeprecated struct{ Msg string }
|
||||
|
||||
func (*IsDeprecated) AFact() {}
|
||||
func (d *IsDeprecated) String() string { return "Deprecated: " + d.Msg }
|
||||
|
||||
type Result struct {
|
||||
Objects map[types.Object]*IsDeprecated
|
||||
Packages map[*types.Package]*IsDeprecated
|
||||
}
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "fact_deprecated",
|
||||
Doc: "Mark deprecated objects",
|
||||
Run: deprecated,
|
||||
FactTypes: []analysis.Fact{(*IsDeprecated)(nil)},
|
||||
ResultType: reflect.TypeOf(Result{}),
|
||||
}
|
||||
|
||||
func deprecated(pass *analysis.Pass) (interface{}, error) {
|
||||
var names []*ast.Ident
|
||||
|
||||
extractDeprecatedMessage := func(docs []*ast.CommentGroup) string {
|
||||
for _, doc := range docs {
|
||||
if doc == nil {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(doc.Text(), "\n\n")
|
||||
for _, part := range parts {
|
||||
if !strings.HasPrefix(part, "Deprecated: ") {
|
||||
continue
|
||||
}
|
||||
alt := part[len("Deprecated: "):]
|
||||
alt = strings.Replace(alt, "\n", " ", -1)
|
||||
return alt
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
doDocs := func(names []*ast.Ident, docs []*ast.CommentGroup) {
|
||||
alt := extractDeprecatedMessage(docs)
|
||||
if alt == "" {
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
obj := pass.TypesInfo.ObjectOf(name)
|
||||
pass.ExportObjectFact(obj, &IsDeprecated{alt})
|
||||
}
|
||||
}
|
||||
|
||||
var docs []*ast.CommentGroup
|
||||
for _, f := range pass.Files {
|
||||
docs = append(docs, f.Doc)
|
||||
}
|
||||
if alt := extractDeprecatedMessage(docs); alt != "" {
|
||||
// Don't mark package syscall as deprecated, even though
|
||||
// it is. A lot of people still use it for simple
|
||||
// constants like SIGKILL, and I am not comfortable
|
||||
// telling them to use x/sys for that.
|
||||
if pass.Pkg.Path() != "syscall" {
|
||||
pass.ExportPackageFact(&IsDeprecated{alt})
|
||||
}
|
||||
}
|
||||
|
||||
docs = docs[:0]
|
||||
for _, f := range pass.Files {
|
||||
fn := func(node ast.Node) bool {
|
||||
if node == nil {
|
||||
return true
|
||||
}
|
||||
var ret bool
|
||||
switch node := node.(type) {
|
||||
case *ast.GenDecl:
|
||||
switch node.Tok {
|
||||
case token.TYPE, token.CONST, token.VAR:
|
||||
docs = append(docs, node.Doc)
|
||||
for i := range node.Specs {
|
||||
switch n := node.Specs[i].(type) {
|
||||
case *ast.ValueSpec:
|
||||
names = append(names, n.Names...)
|
||||
case *ast.TypeSpec:
|
||||
names = append(names, n.Name)
|
||||
}
|
||||
}
|
||||
ret = true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
docs = append(docs, node.Doc)
|
||||
names = []*ast.Ident{node.Name}
|
||||
ret = false
|
||||
case *ast.TypeSpec:
|
||||
docs = append(docs, node.Doc)
|
||||
names = []*ast.Ident{node.Name}
|
||||
ret = true
|
||||
case *ast.ValueSpec:
|
||||
docs = append(docs, node.Doc)
|
||||
names = node.Names
|
||||
ret = false
|
||||
case *ast.File:
|
||||
return true
|
||||
case *ast.StructType:
|
||||
for _, field := range node.Fields.List {
|
||||
doDocs(field.Names, []*ast.CommentGroup{field.Doc})
|
||||
}
|
||||
return false
|
||||
case *ast.InterfaceType:
|
||||
for _, field := range node.Methods.List {
|
||||
doDocs(field.Names, []*ast.CommentGroup{field.Doc})
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if len(names) == 0 || len(docs) == 0 {
|
||||
return ret
|
||||
}
|
||||
doDocs(names, docs)
|
||||
|
||||
docs = docs[:0]
|
||||
names = nil
|
||||
return ret
|
||||
}
|
||||
ast.Inspect(f, fn)
|
||||
}
|
||||
|
||||
out := Result{
|
||||
Objects: map[types.Object]*IsDeprecated{},
|
||||
Packages: map[*types.Package]*IsDeprecated{},
|
||||
}
|
||||
|
||||
for _, fact := range pass.AllObjectFacts() {
|
||||
out.Objects[fact.Object] = fact.Fact.(*IsDeprecated)
|
||||
}
|
||||
for _, fact := range pass.AllPackageFacts() {
|
||||
out.Packages[fact.Package] = fact.Fact.(*IsDeprecated)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package deprecated
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
)
|
||||
|
||||
func TestDeprecated(t *testing.T) {
|
||||
analysistest.Run(t, analysistest.TestData(), Analyzer, "example.com/Deprecated")
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package pkg
|
||||
|
||||
// Deprecated: Don't use this.
|
||||
func fn2() { // want fn2:`Deprecated: Don't use this\.`
|
||||
}
|
||||
|
||||
// This is a function.
|
||||
//
|
||||
// Deprecated: Don't use this.
|
||||
//
|
||||
// Here is how you might use it instead.
|
||||
func fn3() { // want fn3:`Deprecated: Don't use this\.`
|
||||
}
|
||||
|
||||
// Handle cases like:
|
||||
//
|
||||
// Taken from "os" package:
|
||||
//
|
||||
// ```
|
||||
// // Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
|
||||
// const (
|
||||
// SEEK_SET int = 0 // seek relative to the origin of the file
|
||||
// SEEK_CUR int = 1 // seek relative to the current offset
|
||||
// SEEK_END int = 2 // seek relative to the end
|
||||
// )
|
||||
// ```
|
||||
//
|
||||
// Here all three consts i.e., os.SEEK_SET, os.SEEK_CUR and os.SEEK_END are
|
||||
// deprecated and not just os.SEEK_SET.
|
||||
|
||||
// Deprecated: Don't use this.
|
||||
var (
|
||||
SEEK_A = 0 // want SEEK_A:`Deprecated: Don't use this\.`
|
||||
SEEK_B = 1 // want SEEK_B:`Deprecated: Don't use this\.`
|
||||
SEEK_C = 2 // want SEEK_C:`Deprecated: Don't use this\.`
|
||||
)
|
||||
|
||||
// Deprecated: Don't use this.
|
||||
type (
|
||||
pair struct{ x, y int } // want pair:`Deprecated: Don't use this\.`
|
||||
cube struct{ x, y, z int } // want cube:`Deprecated: Don't use this\.`
|
||||
)
|
||||
|
||||
// Deprecated: Don't use this.
|
||||
var SEEK_D = 3 // want SEEK_D:`Deprecated: Don't use this\.`
|
||||
var SEEK_E = 4
|
||||
var SEEK_F = 5
|
||||
@@ -0,0 +1,20 @@
|
||||
package directives
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"honnef.co/go/tools/analysis/lint"
|
||||
)
|
||||
|
||||
func directives(pass *analysis.Pass) (interface{}, error) {
|
||||
return lint.ParseDirectives(pass.Files, pass.Fset), nil
|
||||
}
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "directives",
|
||||
Doc: "extracts linter directives",
|
||||
Run: directives,
|
||||
RunDespiteErrors: true,
|
||||
ResultType: reflect.TypeOf([]lint.Directive{}),
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package generated
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
type Generator int
|
||||
|
||||
// A list of known generators we can detect
|
||||
const (
|
||||
Unknown Generator = iota
|
||||
Goyacc
|
||||
Cgo
|
||||
Stringer
|
||||
ProtocGenGo
|
||||
)
|
||||
|
||||
var (
|
||||
// used by cgo before Go 1.11
|
||||
oldCgo = []byte("// Created by cgo - DO NOT EDIT")
|
||||
prefix = []byte("// Code generated ")
|
||||
suffix = []byte(" DO NOT EDIT.")
|
||||
nl = []byte("\n")
|
||||
crnl = []byte("\r\n")
|
||||
)
|
||||
|
||||
func isGenerated(path string) (Generator, bool) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
defer f.Close()
|
||||
br := bufio.NewReader(f)
|
||||
for {
|
||||
s, err := br.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, false
|
||||
}
|
||||
s = bytes.TrimSuffix(s, crnl)
|
||||
s = bytes.TrimSuffix(s, nl)
|
||||
if bytes.HasPrefix(s, prefix) && bytes.HasSuffix(s, suffix) {
|
||||
if len(s)-len(suffix) < len(prefix) {
|
||||
return Unknown, true
|
||||
}
|
||||
|
||||
text := string(s[len(prefix) : len(s)-len(suffix)])
|
||||
switch text {
|
||||
case "by goyacc.":
|
||||
return Goyacc, true
|
||||
case "by cmd/cgo;":
|
||||
return Cgo, true
|
||||
case "by protoc-gen-go.":
|
||||
return ProtocGenGo, true
|
||||
}
|
||||
if strings.HasPrefix(text, `by "stringer `) {
|
||||
return Stringer, true
|
||||
}
|
||||
if strings.HasPrefix(text, `by goyacc `) {
|
||||
return Goyacc, true
|
||||
}
|
||||
|
||||
return Unknown, true
|
||||
}
|
||||
if bytes.Equal(s, oldCgo) {
|
||||
return Cgo, true
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "isgenerated",
|
||||
Doc: "annotate file names that have been code generated",
|
||||
Run: func(pass *analysis.Pass) (interface{}, error) {
|
||||
m := map[string]Generator{}
|
||||
for _, f := range pass.Files {
|
||||
path := pass.Fset.PositionFor(f.Pos(), false).Filename
|
||||
g, ok := isGenerated(path)
|
||||
if ok {
|
||||
m[path] = g
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
},
|
||||
RunDespiteErrors: true,
|
||||
ResultType: reflect.TypeOf(map[string]Generator{}),
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package nilness
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"reflect"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
"honnef.co/go/tools/internal/passes/buildir"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
// neverReturnsNilFact denotes that a function's return value will never
|
||||
// be nil (typed or untyped). The analysis errs on the side of false
|
||||
// negatives.
|
||||
type neverReturnsNilFact struct {
|
||||
Rets []neverNilness
|
||||
}
|
||||
|
||||
func (*neverReturnsNilFact) AFact() {}
|
||||
func (fact *neverReturnsNilFact) String() string {
|
||||
return fmt.Sprintf("never returns nil: %v", fact.Rets)
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
m map[*types.Func][]neverNilness
|
||||
}
|
||||
|
||||
var Analysis = &analysis.Analyzer{
|
||||
Name: "nilness",
|
||||
Doc: "Annotates return values that will never be nil (typed or untyped)",
|
||||
Run: run,
|
||||
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
||||
FactTypes: []analysis.Fact{(*neverReturnsNilFact)(nil)},
|
||||
ResultType: reflect.TypeOf((*Result)(nil)),
|
||||
}
|
||||
|
||||
// MayReturnNil reports whether the ret's return value of fn might be
|
||||
// a typed or untyped nil value. The value of ret is zero-based. When
|
||||
// globalOnly is true, the only possible nil values are global
|
||||
// variables.
|
||||
//
|
||||
// The analysis has false positives: MayReturnNil can incorrectly
|
||||
// report true, but never incorrectly reports false.
|
||||
func (r *Result) MayReturnNil(fn *types.Func, ret int) (yes bool, globalOnly bool) {
|
||||
if !typeutil.IsPointerLike(fn.Type().(*types.Signature).Results().At(ret).Type()) {
|
||||
return false, false
|
||||
}
|
||||
if len(r.m[fn]) == 0 {
|
||||
return true, false
|
||||
}
|
||||
|
||||
v := r.m[fn][ret]
|
||||
return v != neverNil, v == onlyGlobal
|
||||
}
|
||||
|
||||
func run(pass *analysis.Pass) (interface{}, error) {
|
||||
seen := map[*ir.Function]struct{}{}
|
||||
out := &Result{
|
||||
m: map[*types.Func][]neverNilness{},
|
||||
}
|
||||
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
||||
impl(pass, fn, seen)
|
||||
}
|
||||
|
||||
for _, fact := range pass.AllObjectFacts() {
|
||||
out.m[fact.Object.(*types.Func)] = fact.Fact.(*neverReturnsNilFact).Rets
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type neverNilness uint8
|
||||
|
||||
const (
|
||||
neverNil neverNilness = 1
|
||||
onlyGlobal neverNilness = 2
|
||||
nilly neverNilness = 3
|
||||
)
|
||||
|
||||
func (n neverNilness) String() string {
|
||||
switch n {
|
||||
case neverNil:
|
||||
return "never"
|
||||
case onlyGlobal:
|
||||
return "global"
|
||||
case nilly:
|
||||
return "nil"
|
||||
default:
|
||||
return "BUG"
|
||||
}
|
||||
}
|
||||
|
||||
func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) []neverNilness {
|
||||
if fn.Object() == nil {
|
||||
// TODO(dh): support closures
|
||||
return nil
|
||||
}
|
||||
if fact := new(neverReturnsNilFact); pass.ImportObjectFact(fn.Object(), fact) {
|
||||
return fact.Rets
|
||||
}
|
||||
if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
|
||||
return nil
|
||||
}
|
||||
if fn.Blocks == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := seenFns[fn]; ok {
|
||||
// break recursion
|
||||
return nil
|
||||
}
|
||||
|
||||
seenFns[fn] = struct{}{}
|
||||
|
||||
seen := map[ir.Value]struct{}{}
|
||||
|
||||
var mightReturnNil func(v ir.Value) neverNilness
|
||||
mightReturnNil = func(v ir.Value) neverNilness {
|
||||
if _, ok := seen[v]; ok {
|
||||
// break cycle
|
||||
return nilly
|
||||
}
|
||||
if !typeutil.IsPointerLike(v.Type()) {
|
||||
return neverNil
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
switch v := v.(type) {
|
||||
case *ir.MakeInterface:
|
||||
return mightReturnNil(v.X)
|
||||
case *ir.Convert:
|
||||
return mightReturnNil(v.X)
|
||||
case *ir.SliceToArrayPointer:
|
||||
if typeutil.CoreType(v.Type()).(*types.Pointer).Elem().Underlying().(*types.Array).Len() == 0 {
|
||||
return mightReturnNil(v.X)
|
||||
} else {
|
||||
// converting a slice to an array pointer of length > 0 panics if the slice is nil
|
||||
return neverNil
|
||||
}
|
||||
case *ir.Slice:
|
||||
return mightReturnNil(v.X)
|
||||
case *ir.Phi:
|
||||
ret := neverNil
|
||||
for _, e := range v.Edges {
|
||||
if n := mightReturnNil(e); n > ret {
|
||||
ret = n
|
||||
}
|
||||
}
|
||||
return ret
|
||||
case *ir.Extract:
|
||||
switch d := v.Tuple.(type) {
|
||||
case *ir.Call:
|
||||
if callee := d.Call.StaticCallee(); callee != nil {
|
||||
ret := impl(pass, callee, seenFns)
|
||||
if len(ret) == 0 {
|
||||
return nilly
|
||||
}
|
||||
return ret[v.Index]
|
||||
} else {
|
||||
return nilly
|
||||
}
|
||||
case *ir.TypeAssert, *ir.Next, *ir.Select, *ir.MapLookup, *ir.TypeSwitch, *ir.Recv, *ir.Sigma:
|
||||
// we don't need to look at the Extract's index
|
||||
// because we've already checked its type.
|
||||
return nilly
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled type %T", d))
|
||||
}
|
||||
case *ir.Call:
|
||||
if callee := v.Call.StaticCallee(); callee != nil {
|
||||
ret := impl(pass, callee, seenFns)
|
||||
if len(ret) == 0 {
|
||||
return nilly
|
||||
}
|
||||
return ret[0]
|
||||
} else {
|
||||
return nilly
|
||||
}
|
||||
case *ir.BinOp, *ir.UnOp, *ir.Alloc, *ir.FieldAddr, *ir.IndexAddr, *ir.Global, *ir.MakeSlice, *ir.MakeClosure, *ir.Function, *ir.MakeMap, *ir.MakeChan:
|
||||
return neverNil
|
||||
case *ir.Sigma:
|
||||
iff, ok := v.From.Control().(*ir.If)
|
||||
if !ok {
|
||||
return nilly
|
||||
}
|
||||
binop, ok := iff.Cond.(*ir.BinOp)
|
||||
if !ok {
|
||||
return nilly
|
||||
}
|
||||
isNil := func(v ir.Value) bool {
|
||||
k, ok := v.(*ir.Const)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return k.Value == nil
|
||||
}
|
||||
if binop.X == v.X && isNil(binop.Y) || binop.Y == v.X && isNil(binop.X) {
|
||||
op := binop.Op
|
||||
if v.From.Succs[0] != v.Block() {
|
||||
// we're in the false branch, negate op
|
||||
switch op {
|
||||
case token.EQL:
|
||||
op = token.NEQ
|
||||
case token.NEQ:
|
||||
op = token.EQL
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled token %v", op))
|
||||
}
|
||||
}
|
||||
switch op {
|
||||
case token.EQL:
|
||||
return nilly
|
||||
case token.NEQ:
|
||||
return neverNil
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled token %v", op))
|
||||
}
|
||||
}
|
||||
return nilly
|
||||
case *ir.ChangeType:
|
||||
return mightReturnNil(v.X)
|
||||
case *ir.Load:
|
||||
if _, ok := v.X.(*ir.Global); ok {
|
||||
return onlyGlobal
|
||||
}
|
||||
return nilly
|
||||
case *ir.AggregateConst:
|
||||
return neverNil
|
||||
case *ir.TypeAssert, *ir.ChangeInterface, *ir.Field, *ir.Const, *ir.GenericConst, *ir.Index, *ir.MapLookup, *ir.Parameter, *ir.Recv, *ir.TypeSwitch:
|
||||
return nilly
|
||||
case *ir.CompositeValue:
|
||||
// We can get here via composite literals of type parameters, for which typeutil.IsPointerLike doesn't
|
||||
// currently return false (see https://staticcheck.io/issues/1364). However, we only emit ir.CompositeValue
|
||||
// for value types, so we know it can't be nil.
|
||||
return neverNil
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled type %T", v))
|
||||
}
|
||||
}
|
||||
ret := fn.Exit.Control().(*ir.Return)
|
||||
out := make([]neverNilness, len(ret.Results))
|
||||
export := false
|
||||
for i, v := range ret.Results {
|
||||
v := mightReturnNil(v)
|
||||
out[i] = v
|
||||
if v != nilly && typeutil.IsPointerLike(fn.Signature.Results().At(i).Type()) {
|
||||
export = true
|
||||
}
|
||||
}
|
||||
if export {
|
||||
pass.ExportObjectFact(fn.Object(), &neverReturnsNilFact{out})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package nilness
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
)
|
||||
|
||||
func TestNilness(t *testing.T) {
|
||||
analysistest.Run(t, analysistest.TestData(), Analysis, "example.com/Nilness")
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package pkg
|
||||
|
||||
import "errors"
|
||||
|
||||
type T struct{ f *int }
|
||||
type T2 T
|
||||
|
||||
func fn1() *T {
|
||||
if true {
|
||||
return nil
|
||||
}
|
||||
return &T{}
|
||||
}
|
||||
|
||||
func fn2() *T { // want fn2:`never returns nil: \[never\]`
|
||||
return &T{}
|
||||
}
|
||||
|
||||
func fn3() *T { // want fn3:`never returns nil: \[never\]`
|
||||
return new(T)
|
||||
}
|
||||
|
||||
func fn4() *T { // want fn4:`never returns nil: \[never\]`
|
||||
return fn3()
|
||||
}
|
||||
|
||||
func fn5() *T {
|
||||
return fn1()
|
||||
}
|
||||
|
||||
func fn6() *T2 { // want fn6:`never returns nil: \[never\]`
|
||||
return (*T2)(fn4())
|
||||
}
|
||||
|
||||
func fn7() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fn8() interface{} { // want fn8:`never returns nil: \[never\]`
|
||||
return 1
|
||||
}
|
||||
|
||||
func fn9() []int { // want fn9:`never returns nil: \[never\]`
|
||||
x := []int{}
|
||||
y := x[:1]
|
||||
return y
|
||||
}
|
||||
|
||||
func fn10(x []int) []int {
|
||||
return x[:1]
|
||||
}
|
||||
|
||||
func fn11(x *T) *T {
|
||||
return x
|
||||
}
|
||||
|
||||
func fn12(x *T) *int {
|
||||
return x.f
|
||||
}
|
||||
|
||||
func fn13() *int { // want fn13:`never returns nil: \[never\]`
|
||||
return new(int)
|
||||
}
|
||||
|
||||
func fn14() []int { // want fn14:`never returns nil: \[never\]`
|
||||
return make([]int, 0)
|
||||
}
|
||||
|
||||
func fn15() []int { // want fn15:`never returns nil: \[never\]`
|
||||
return []int{}
|
||||
}
|
||||
|
||||
func fn16() []int {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fn17() error {
|
||||
if true {
|
||||
return errors.New("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fn18() (err error) { // want fn18:`never returns nil: \[never\]`
|
||||
for {
|
||||
if err = fn17(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var x *int
|
||||
|
||||
func fn19() *int { // want fn19:`never returns nil: \[global\]`
|
||||
return x
|
||||
}
|
||||
|
||||
func fn20() *int {
|
||||
if true {
|
||||
return x
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fn27[T ~struct{ F int }]() T { // want fn27:`never returns nil: \[never\]`
|
||||
return T{0}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
//go:build go1.17
|
||||
// +build go1.17
|
||||
|
||||
package pkg
|
||||
|
||||
func fn21() *[5]int { // want fn21:`never returns nil: \[never\]`
|
||||
var x []int
|
||||
return (*[5]int)(x)
|
||||
}
|
||||
|
||||
func fn22() *[0]int {
|
||||
var x []int
|
||||
return (*[0]int)(x)
|
||||
}
|
||||
|
||||
func fn23() *[5]int { // want fn23:`never returns nil: \[never\]`
|
||||
var x []int
|
||||
type T [5]int
|
||||
ret := (*T)(x)
|
||||
return (*[5]int)(ret)
|
||||
}
|
||||
|
||||
func fn24() *[0]int {
|
||||
var x []int
|
||||
type T [0]int
|
||||
ret := (*T)(x)
|
||||
return (*[0]int)(ret)
|
||||
}
|
||||
|
||||
func fn25() *[5]int { // want fn25:`never returns nil: \[never\]`
|
||||
var x []int
|
||||
type T *[5]int
|
||||
return (T)(x)
|
||||
}
|
||||
|
||||
func fn26() *[0]int {
|
||||
var x []int
|
||||
type T *[0]int
|
||||
return (T)(x)
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package purity
|
||||
|
||||
// TODO(dh): we should split this into two facts, one tracking actual purity, and one tracking side-effects. A function
|
||||
// that returns a heap allocation isn't pure, but it may be free of side effects.
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
"reflect"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
"honnef.co/go/tools/internal/passes/buildir"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
type IsPure struct{}
|
||||
|
||||
func (*IsPure) AFact() {}
|
||||
func (d *IsPure) String() string { return "is pure" }
|
||||
|
||||
type Result map[*types.Func]*IsPure
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "fact_purity",
|
||||
Doc: "Mark pure functions",
|
||||
Run: purity,
|
||||
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
||||
FactTypes: []analysis.Fact{(*IsPure)(nil)},
|
||||
ResultType: reflect.TypeOf(Result{}),
|
||||
}
|
||||
|
||||
var pureStdlib = map[string]struct{}{
|
||||
"errors.New": {},
|
||||
"fmt.Errorf": {},
|
||||
"fmt.Sprintf": {},
|
||||
"fmt.Sprint": {},
|
||||
"sort.Reverse": {},
|
||||
"strings.Map": {},
|
||||
"strings.Repeat": {},
|
||||
"strings.Replace": {},
|
||||
"strings.Title": {},
|
||||
"strings.ToLower": {},
|
||||
"strings.ToLowerSpecial": {},
|
||||
"strings.ToTitle": {},
|
||||
"strings.ToTitleSpecial": {},
|
||||
"strings.ToUpper": {},
|
||||
"strings.ToUpperSpecial": {},
|
||||
"strings.Trim": {},
|
||||
"strings.TrimFunc": {},
|
||||
"strings.TrimLeft": {},
|
||||
"strings.TrimLeftFunc": {},
|
||||
"strings.TrimPrefix": {},
|
||||
"strings.TrimRight": {},
|
||||
"strings.TrimRightFunc": {},
|
||||
"strings.TrimSpace": {},
|
||||
"strings.TrimSuffix": {},
|
||||
"(*net/http.Request).WithContext": {},
|
||||
"time.Now": {},
|
||||
"time.Parse": {},
|
||||
"time.ParseInLocation": {},
|
||||
"time.Unix": {},
|
||||
"time.UnixMicro": {},
|
||||
"time.UnixMilli": {},
|
||||
"(time.Time).Add": {},
|
||||
"(time.Time).AddDate": {},
|
||||
"(time.Time).After": {},
|
||||
"(time.Time).Before": {},
|
||||
"(time.Time).Clock": {},
|
||||
"(time.Time).Compare": {},
|
||||
"(time.Time).Date": {},
|
||||
"(time.Time).Day": {},
|
||||
"(time.Time).Equal": {},
|
||||
"(time.Time).Format": {},
|
||||
"(time.Time).GoString": {},
|
||||
"(time.Time).GobEncode": {},
|
||||
"(time.Time).Hour": {},
|
||||
"(time.Time).ISOWeek": {},
|
||||
"(time.Time).In": {},
|
||||
"(time.Time).IsDST": {},
|
||||
"(time.Time).IsZero": {},
|
||||
"(time.Time).Local": {},
|
||||
"(time.Time).Location": {},
|
||||
"(time.Time).MarshalBinary": {},
|
||||
"(time.Time).MarshalJSON": {},
|
||||
"(time.Time).MarshalText": {},
|
||||
"(time.Time).Minute": {},
|
||||
"(time.Time).Month": {},
|
||||
"(time.Time).Nanosecond": {},
|
||||
"(time.Time).Round": {},
|
||||
"(time.Time).Second": {},
|
||||
"(time.Time).String": {},
|
||||
"(time.Time).Sub": {},
|
||||
"(time.Time).Truncate": {},
|
||||
"(time.Time).UTC": {},
|
||||
"(time.Time).Unix": {},
|
||||
"(time.Time).UnixMicro": {},
|
||||
"(time.Time).UnixMilli": {},
|
||||
"(time.Time).UnixNano": {},
|
||||
"(time.Time).Weekday": {},
|
||||
"(time.Time).Year": {},
|
||||
"(time.Time).YearDay": {},
|
||||
"(time.Time).Zone": {},
|
||||
"(time.Time).ZoneBounds": {},
|
||||
}
|
||||
|
||||
func purity(pass *analysis.Pass) (interface{}, error) {
|
||||
seen := map[*ir.Function]struct{}{}
|
||||
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
|
||||
var check func(fn *ir.Function) (ret bool)
|
||||
check = func(fn *ir.Function) (ret bool) {
|
||||
if fn.Object() == nil {
|
||||
// TODO(dh): support closures
|
||||
return false
|
||||
}
|
||||
if pass.ImportObjectFact(fn.Object(), new(IsPure)) {
|
||||
return true
|
||||
}
|
||||
if fn.Pkg != irpkg {
|
||||
// Function is in another package but wasn't marked as
|
||||
// pure, ergo it isn't pure
|
||||
return false
|
||||
}
|
||||
// Break recursion
|
||||
if _, ok := seen[fn]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
seen[fn] = struct{}{}
|
||||
defer func() {
|
||||
if ret {
|
||||
pass.ExportObjectFact(fn.Object(), &IsPure{})
|
||||
}
|
||||
}()
|
||||
|
||||
if irutil.IsStub(fn) {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := pureStdlib[fn.Object().(*types.Func).FullName()]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if fn.Signature.Results().Len() == 0 {
|
||||
// A function with no return values is empty or is doing some
|
||||
// work we cannot see (for example because of build tags);
|
||||
// don't consider it pure.
|
||||
return false
|
||||
}
|
||||
|
||||
var isBasic func(typ types.Type) bool
|
||||
isBasic = func(typ types.Type) bool {
|
||||
switch u := typ.Underlying().(type) {
|
||||
case *types.Basic:
|
||||
return true
|
||||
case *types.Struct:
|
||||
for i := 0; i < u.NumFields(); i++ {
|
||||
if !isBasic(u.Field(i).Type()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, param := range fn.Params {
|
||||
// TODO(dh): this may not be strictly correct. pure code can, to an extent, operate on non-basic types.
|
||||
if !isBasic(param.Type()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Don't consider external functions pure.
|
||||
if fn.Blocks == nil {
|
||||
return false
|
||||
}
|
||||
checkCall := func(common *ir.CallCommon) bool {
|
||||
if common.IsInvoke() {
|
||||
return false
|
||||
}
|
||||
builtin, ok := common.Value.(*ir.Builtin)
|
||||
if !ok {
|
||||
if common.StaticCallee() != fn {
|
||||
if common.StaticCallee() == nil {
|
||||
return false
|
||||
}
|
||||
if !check(common.StaticCallee()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch builtin.Name() {
|
||||
case "len", "cap":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var isStackAddr func(ir.Value) bool
|
||||
isStackAddr = func(v ir.Value) bool {
|
||||
switch v := v.(type) {
|
||||
case *ir.Alloc:
|
||||
return !v.Heap
|
||||
case *ir.FieldAddr:
|
||||
return isStackAddr(v.X)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, b := range fn.Blocks {
|
||||
for _, ins := range b.Instrs {
|
||||
switch ins := ins.(type) {
|
||||
case *ir.Call:
|
||||
if !checkCall(ins.Common()) {
|
||||
return false
|
||||
}
|
||||
case *ir.Defer:
|
||||
if !checkCall(&ins.Call) {
|
||||
return false
|
||||
}
|
||||
case *ir.Select:
|
||||
return false
|
||||
case *ir.Send:
|
||||
return false
|
||||
case *ir.Go:
|
||||
return false
|
||||
case *ir.Panic:
|
||||
return false
|
||||
case *ir.Store:
|
||||
if !isStackAddr(ins.Addr) {
|
||||
return false
|
||||
}
|
||||
case *ir.FieldAddr:
|
||||
if !isStackAddr(ins.X) {
|
||||
return false
|
||||
}
|
||||
case *ir.Alloc:
|
||||
// TODO(dh): make use of proper escape analysis
|
||||
if ins.Heap {
|
||||
return false
|
||||
}
|
||||
case *ir.Load:
|
||||
if !isStackAddr(ins.X) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
||||
check(fn)
|
||||
}
|
||||
|
||||
out := Result{}
|
||||
for _, fact := range pass.AllObjectFacts() {
|
||||
out[fact.Object.(*types.Func)] = fact.Fact.(*IsPure)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package purity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
)
|
||||
|
||||
func TestPurity(t *testing.T) {
|
||||
analysistest.Run(t, analysistest.TestData(), Analyzer, "example.com/Purity")
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package pkg
|
||||
|
||||
func foo(a, b int) int { return a + b } // want foo:"is pure"
|
||||
func bar(a, b int) int {
|
||||
println(a + b)
|
||||
return a + b
|
||||
}
|
||||
|
||||
func empty() {}
|
||||
func stubPointer() *int { return nil }
|
||||
func stubInt() int { return 0 }
|
||||
|
||||
func fn3() {
|
||||
empty()
|
||||
stubPointer()
|
||||
stubInt()
|
||||
}
|
||||
|
||||
func ptr1() *int { return new(int) }
|
||||
func ptr2() *int { var x int; return &x }
|
||||
func lit() []int { return []int{} }
|
||||
|
||||
var X int
|
||||
|
||||
func load() int { _ = X; return 0 }
|
||||
func assign(x int) int { _ = x; return 0 } // want assign:"is pure"
|
||||
|
||||
type pureStruct1 struct {
|
||||
a int
|
||||
b string
|
||||
pureStruct2
|
||||
}
|
||||
|
||||
type pureStruct2 struct {
|
||||
c float64
|
||||
}
|
||||
|
||||
func (arg pureStruct1) get() int { // want get:"is pure"
|
||||
return arg.a
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package tokenfile
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"reflect"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "tokenfileanalyzer",
|
||||
Doc: "creates a mapping of *token.File to *ast.File",
|
||||
Run: func(pass *analysis.Pass) (interface{}, error) {
|
||||
m := map[*token.File]*ast.File{}
|
||||
for _, af := range pass.Files {
|
||||
tf := pass.Fset.File(af.Pos())
|
||||
m[tf] = af
|
||||
}
|
||||
return m, nil
|
||||
},
|
||||
RunDespiteErrors: true,
|
||||
ResultType: reflect.TypeOf(map[*token.File]*ast.File{}),
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type T struct{ x *int }
|
||||
|
||||
func notAStub() {}
|
||||
|
||||
func fn1() *int { return nil }
|
||||
func fn2() (int, *int, int) { return 0, nil, 0 }
|
||||
|
||||
func fn3() (out1 int, out2 error) { notAStub(); return 0, nil }
|
||||
func fn4() error { notAStub(); return nil }
|
||||
|
||||
func gen2() (out1 interface{}) { // want gen2:`always typed: 00000001`
|
||||
return 1
|
||||
}
|
||||
|
||||
func gen3() (out1 interface{}) { // want gen3:`always typed: 00000001`
|
||||
// flag, always returns a typed value
|
||||
m := map[int]*int{}
|
||||
return m[0]
|
||||
}
|
||||
|
||||
func gen4() (out1 int, out2 interface{}, out3 *int) { // want gen4:`always typed: 00000010`
|
||||
// flag ret[1], always a typed value
|
||||
m := map[int]*int{}
|
||||
return 0, m[0], nil
|
||||
}
|
||||
|
||||
func gen5() (out1 interface{}) { // want gen5:`always typed: 00000001`
|
||||
// flag, propagate gen3
|
||||
return gen3()
|
||||
}
|
||||
|
||||
func gen6(b bool) interface{} {
|
||||
// don't flag, sometimes returns untyped nil
|
||||
if b {
|
||||
m := map[int]*int{}
|
||||
return m[0]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func gen7() (out1 interface{}) { // want gen7:`always typed: 00000001`
|
||||
// flag, always returns a typed value
|
||||
return fn1()
|
||||
}
|
||||
|
||||
func gen8(x *int) (out1 interface{}) { // want gen8:`always typed: 00000001`
|
||||
// flag
|
||||
if x == nil {
|
||||
return x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func gen9() (out1 interface{}) { // want gen9:`always typed: 00000001`
|
||||
// flag
|
||||
var x *int
|
||||
return x
|
||||
}
|
||||
|
||||
func gen10() (out1 interface{}) { // want gen10:`always typed: 00000001`
|
||||
// flag
|
||||
var x *int
|
||||
if x == nil {
|
||||
return x
|
||||
}
|
||||
return errors.New("")
|
||||
}
|
||||
|
||||
func gen11() interface{} {
|
||||
// don't flag, we sometimes return untyped nil
|
||||
if true {
|
||||
return nil
|
||||
} else {
|
||||
return (*int)(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func gen12(b bool) (out1 interface{}) { // want gen12:`always typed: 00000001`
|
||||
// flag, all branches return typed nils
|
||||
var x interface{}
|
||||
if b {
|
||||
x = (*int)(nil)
|
||||
} else {
|
||||
x = (*string)(nil)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func gen13() (out1 interface{}) { // want gen13:`always typed: 00000001`
|
||||
// flag, always returns a typed value
|
||||
_, x, _ := fn2()
|
||||
return x
|
||||
}
|
||||
|
||||
func gen14(ch chan *int) (out1 interface{}) { // want gen14:`always typed: 00000001`
|
||||
// flag
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func gen15() (out1 interface{}) { // want gen15:`always typed: 00000001`
|
||||
// flag
|
||||
t := &T{}
|
||||
return t.x
|
||||
}
|
||||
|
||||
var g *int = new(int)
|
||||
|
||||
func gen16() (out1 interface{}) { // want gen16:`always typed: 00000001`
|
||||
return g
|
||||
}
|
||||
|
||||
func gen17(x interface{}) interface{} {
|
||||
// don't flag
|
||||
if x != nil {
|
||||
return x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func gen18() (int, error) {
|
||||
// don't flag
|
||||
_, err := fn3()
|
||||
if err != nil {
|
||||
return 0, errors.New("yo")
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func gen19() (out interface{}) {
|
||||
// don't flag
|
||||
if true {
|
||||
return (*int)(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func gen20() (out interface{}) {
|
||||
// don't flag
|
||||
if true {
|
||||
return (*int)(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func gen21() error {
|
||||
if false {
|
||||
return (*exec.Error)(nil)
|
||||
}
|
||||
return fn4()
|
||||
}
|
||||
|
||||
func gen22() interface{} {
|
||||
// don't flag, propagate gen6
|
||||
return gen6(false)
|
||||
}
|
||||
|
||||
func gen23() interface{} {
|
||||
return gen24()
|
||||
}
|
||||
|
||||
func gen24() interface{} {
|
||||
return gen23()
|
||||
}
|
||||
|
||||
func gen25(x interface{}) (out1 interface{}) { // want gen25:`always typed: 00000001`
|
||||
return x.(interface{})
|
||||
}
|
||||
|
||||
func gen26(x interface{}) interface{} {
|
||||
v, _ := x.(interface{})
|
||||
return v
|
||||
}
|
||||
|
||||
func gen27(x interface{}) (out1 interface{}) {
|
||||
defer recover()
|
||||
out1 = x.(interface{})
|
||||
return out1
|
||||
}
|
||||
|
||||
type Error struct{}
|
||||
|
||||
func (*Error) Error() string { return "" }
|
||||
|
||||
func gen28() (out1 interface{}) { // want gen28:`always typed: 00000001`
|
||||
x := new(Error)
|
||||
var y error = x
|
||||
return y
|
||||
}
|
||||
|
||||
func gen29() (out1 interface{}) { // want gen29:`always typed: 00000001`
|
||||
var x *Error
|
||||
var y error = x
|
||||
return y
|
||||
}
|
||||
|
||||
func gen30() (out1, out2 interface{}) { // want gen30:`always typed: 00000011`
|
||||
return gen29(), gen28()
|
||||
}
|
||||
|
||||
func gen31() (out1 interface{}) { // want gen31:`always typed: 00000001`
|
||||
a, _ := gen30()
|
||||
return a
|
||||
}
|
||||
|
||||
func gen32() (out1 interface{}) { // want gen32:`always typed: 00000001`
|
||||
_, b := gen30()
|
||||
return b
|
||||
}
|
||||
|
||||
func gen33() (out1 interface{}) { // want gen33:`always typed: 00000001`
|
||||
a, b := gen30()
|
||||
_ = a
|
||||
return b
|
||||
}
|
||||
|
||||
func gen34() (out1, out2 interface{}) { // want gen34:`always typed: 00000010`
|
||||
return nil, 1
|
||||
}
|
||||
|
||||
func gen35() (out1 interface{}) {
|
||||
a, _ := gen34()
|
||||
return a
|
||||
}
|
||||
|
||||
func gen36() (out1 interface{}) { // want gen36:`always typed: 00000001`
|
||||
_, b := gen34()
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package typedness
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"reflect"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
"honnef.co/go/tools/internal/passes/buildir"
|
||||
|
||||
"golang.org/x/exp/typeparams"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
// alwaysTypedFact denotes that a function's return value will never
|
||||
// be untyped nil. The analysis errs on the side of false negatives.
|
||||
type alwaysTypedFact struct {
|
||||
Rets uint8
|
||||
}
|
||||
|
||||
func (*alwaysTypedFact) AFact() {}
|
||||
func (fact *alwaysTypedFact) String() string {
|
||||
return fmt.Sprintf("always typed: %08b", fact.Rets)
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
m map[*types.Func]uint8
|
||||
}
|
||||
|
||||
var Analysis = &analysis.Analyzer{
|
||||
Name: "typedness",
|
||||
Doc: "Annotates return values that are always typed values",
|
||||
Run: run,
|
||||
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
||||
FactTypes: []analysis.Fact{(*alwaysTypedFact)(nil)},
|
||||
ResultType: reflect.TypeOf((*Result)(nil)),
|
||||
}
|
||||
|
||||
// MustReturnTyped reports whether the ret's return value of fn must
|
||||
// be a typed value, i.e. an interface value containing a concrete
|
||||
// type or trivially a concrete type. The value of ret is zero-based.
|
||||
//
|
||||
// The analysis has false negatives: MustReturnTyped may incorrectly
|
||||
// report false, but never incorrectly reports true.
|
||||
func (r *Result) MustReturnTyped(fn *types.Func, ret int) bool {
|
||||
if _, ok := fn.Type().(*types.Signature).Results().At(ret).Type().Underlying().(*types.Interface); !ok {
|
||||
return true
|
||||
}
|
||||
return (r.m[fn] & (1 << ret)) != 0
|
||||
}
|
||||
|
||||
func run(pass *analysis.Pass) (interface{}, error) {
|
||||
seen := map[*ir.Function]struct{}{}
|
||||
out := &Result{
|
||||
m: map[*types.Func]uint8{},
|
||||
}
|
||||
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
||||
impl(pass, fn, seen)
|
||||
}
|
||||
|
||||
for _, fact := range pass.AllObjectFacts() {
|
||||
out.m[fact.Object.(*types.Func)] = fact.Fact.(*alwaysTypedFact).Rets
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) (out uint8) {
|
||||
if fn.Signature.Results().Len() > 8 {
|
||||
return 0
|
||||
}
|
||||
if fn.Object() == nil {
|
||||
// TODO(dh): support closures
|
||||
return 0
|
||||
}
|
||||
if fact := new(alwaysTypedFact); pass.ImportObjectFact(fn.Object(), fact) {
|
||||
return fact.Rets
|
||||
}
|
||||
if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
|
||||
return 0
|
||||
}
|
||||
if fn.Blocks == nil {
|
||||
return 0
|
||||
}
|
||||
if irutil.IsStub(fn) {
|
||||
return 0
|
||||
}
|
||||
if _, ok := seenFns[fn]; ok {
|
||||
// break recursion
|
||||
return 0
|
||||
}
|
||||
|
||||
seenFns[fn] = struct{}{}
|
||||
defer func() {
|
||||
for i := 0; i < fn.Signature.Results().Len(); i++ {
|
||||
if _, ok := fn.Signature.Results().At(i).Type().Underlying().(*types.Interface); !ok {
|
||||
// we don't need facts to know that non-interface
|
||||
// types can't be untyped nil. zeroing out those bits
|
||||
// may result in all bits being zero, in which case we
|
||||
// don't have to save any fact.
|
||||
out &= ^(1 << i)
|
||||
}
|
||||
}
|
||||
if out > 0 {
|
||||
pass.ExportObjectFact(fn.Object(), &alwaysTypedFact{out})
|
||||
}
|
||||
}()
|
||||
|
||||
isUntypedNil := func(v ir.Value) bool {
|
||||
k, ok := v.(*ir.Const)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := k.Type().Underlying().(*types.Interface); !ok {
|
||||
return false
|
||||
}
|
||||
return k.Value == nil
|
||||
}
|
||||
|
||||
var do func(v ir.Value, seen map[ir.Value]struct{}) bool
|
||||
do = func(v ir.Value, seen map[ir.Value]struct{}) bool {
|
||||
if _, ok := seen[v]; ok {
|
||||
// break cycle
|
||||
return false
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
switch v := v.(type) {
|
||||
case *ir.Const:
|
||||
// can't be a typed nil, because then we'd be returning the
|
||||
// result of MakeInterface.
|
||||
return false
|
||||
case *ir.ChangeInterface:
|
||||
return do(v.X, seen)
|
||||
case *ir.Extract:
|
||||
call, ok := v.Tuple.(*ir.Call)
|
||||
if !ok {
|
||||
// We only care about extracts of function results. For
|
||||
// everything else (e.g. channel receives and map
|
||||
// lookups), we can either not deduce any information, or
|
||||
// will see a MakeInterface.
|
||||
return false
|
||||
}
|
||||
if callee := call.Call.StaticCallee(); callee != nil {
|
||||
return impl(pass, callee, seenFns)&(1<<v.Index) != 0
|
||||
} else {
|
||||
// we don't know what function we're calling. no need
|
||||
// to look at the signature, though. if it weren't an
|
||||
// interface, we'd be seeing a MakeInterface
|
||||
// instruction.
|
||||
return false
|
||||
}
|
||||
case *ir.Call:
|
||||
if callee := v.Call.StaticCallee(); callee != nil {
|
||||
return impl(pass, callee, seenFns)&1 != 0
|
||||
} else {
|
||||
// we don't know what function we're calling. no need
|
||||
// to look at the signature, though. if it weren't an
|
||||
// interface, we'd be seeing a MakeInterface
|
||||
// instruction.
|
||||
return false
|
||||
}
|
||||
case *ir.Sigma:
|
||||
iff, ok := v.From.Control().(*ir.If)
|
||||
if !ok {
|
||||
// give up
|
||||
return false
|
||||
}
|
||||
|
||||
binop, ok := iff.Cond.(*ir.BinOp)
|
||||
if !ok {
|
||||
// give up
|
||||
return false
|
||||
}
|
||||
|
||||
if (binop.X == v.X && isUntypedNil(binop.Y)) || (isUntypedNil(binop.X) && binop.Y == v.X) {
|
||||
op := binop.Op
|
||||
if v.From.Succs[0] != v.Block() {
|
||||
// we're in the false branch, negate op
|
||||
switch op {
|
||||
case token.EQL:
|
||||
op = token.NEQ
|
||||
case token.NEQ:
|
||||
op = token.EQL
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled token %v", op))
|
||||
}
|
||||
}
|
||||
|
||||
switch op {
|
||||
case token.EQL:
|
||||
// returned value equals untyped nil
|
||||
return false
|
||||
case token.NEQ:
|
||||
// returned value does not equal untyped nil
|
||||
return true
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled token %v", op))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dh): handle comparison with typed nil
|
||||
|
||||
// give up
|
||||
return false
|
||||
case *ir.Phi:
|
||||
for _, pv := range v.Edges {
|
||||
if !do(pv, seen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *ir.MakeInterface:
|
||||
terms, err := typeparams.NormalTerms(v.X.Type())
|
||||
if len(terms) == 0 || err != nil {
|
||||
// Type is a type parameter with no type terms (or we couldn't determine the terms). Such a type
|
||||
// _can_ be nil when put in an interface value.
|
||||
//
|
||||
// There is no instruction that can create a guaranteed non-nil instance of a type parameter without
|
||||
// type constraints, so we return false right away, without checking v.X's typedness.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
case *ir.TypeAssert:
|
||||
// type assertions fail for untyped nils. Either we have a
|
||||
// single lhs and the type assertion succeeds or panics,
|
||||
// or we have two lhs and we'll return Extract instead.
|
||||
return true
|
||||
case *ir.ChangeType:
|
||||
// we'll only see interface->interface conversions, which
|
||||
// don't tell us anything about the nilness.
|
||||
return false
|
||||
case *ir.MapLookup, *ir.Index, *ir.Recv, *ir.Parameter, *ir.Load, *ir.Field:
|
||||
// All other instructions that tell us nothing about the
|
||||
// typedness of interface values.
|
||||
return false
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled type %T", v))
|
||||
}
|
||||
}
|
||||
|
||||
ret := fn.Exit.Control().(*ir.Return)
|
||||
for i, v := range ret.Results {
|
||||
typ := fn.Signature.Results().At(i).Type()
|
||||
if _, ok := typ.Underlying().(*types.Interface); ok && !typeparams.IsTypeParam(typ) {
|
||||
if do(v, map[ir.Value]struct{}{}) {
|
||||
out |= 1 << i
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package typedness
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
)
|
||||
|
||||
func TestTypedness(t *testing.T) {
|
||||
analysistest.Run(t, analysistest.TestData(), Analysis, "example.com/Typedness")
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// Package lint provides abstractions on top of go/analysis.
|
||||
// These abstractions add extra information to analyzes, such as structured documentation and severities.
|
||||
package lint
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
// Analyzer wraps a go/analysis.Analyzer and provides structured documentation.
|
||||
type Analyzer struct {
|
||||
// The analyzer's documentation. Unlike go/analysis.Analyzer.Doc,
|
||||
// this field is structured, providing access to severity, options
|
||||
// etc.
|
||||
Doc *Documentation
|
||||
Analyzer *analysis.Analyzer
|
||||
}
|
||||
|
||||
func (a *Analyzer) initialize() {
|
||||
a.Analyzer.Doc = a.Doc.String()
|
||||
if a.Analyzer.Flags.Usage == nil {
|
||||
fs := flag.NewFlagSet("", flag.PanicOnError)
|
||||
fs.Var(newVersionFlag(), "go", "Target Go version")
|
||||
a.Analyzer.Flags = *fs
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeAnalyzers takes a map of documentation and a map of go/analysis.Analyzers and returns a slice of Analyzers.
|
||||
// The map keys are the analyzer names.
|
||||
func InitializeAnalyzers(docs map[string]*Documentation, analyzers map[string]*analysis.Analyzer) []*Analyzer {
|
||||
out := make([]*Analyzer, 0, len(analyzers))
|
||||
for k, v := range analyzers {
|
||||
v.Name = k
|
||||
a := &Analyzer{
|
||||
Doc: docs[k],
|
||||
Analyzer: v,
|
||||
}
|
||||
a.initialize()
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Severity describes the severity of diagnostics reported by an analyzer.
|
||||
type Severity int
|
||||
|
||||
const (
|
||||
SeverityNone Severity = iota
|
||||
SeverityError
|
||||
SeverityDeprecated
|
||||
SeverityWarning
|
||||
SeverityInfo
|
||||
SeverityHint
|
||||
)
|
||||
|
||||
// MergeStrategy sets how merge mode should behave for diagnostics of an analyzer.
|
||||
type MergeStrategy int
|
||||
|
||||
const (
|
||||
MergeIfAny MergeStrategy = iota
|
||||
MergeIfAll
|
||||
)
|
||||
|
||||
type RawDocumentation struct {
|
||||
Title string
|
||||
Text string
|
||||
Before string
|
||||
After string
|
||||
Since string
|
||||
NonDefault bool
|
||||
Options []string
|
||||
Severity Severity
|
||||
MergeIf MergeStrategy
|
||||
}
|
||||
|
||||
type Documentation struct {
|
||||
Title string
|
||||
Text string
|
||||
|
||||
TitleMarkdown string
|
||||
TextMarkdown string
|
||||
|
||||
Before string
|
||||
After string
|
||||
Since string
|
||||
NonDefault bool
|
||||
Options []string
|
||||
Severity Severity
|
||||
MergeIf MergeStrategy
|
||||
}
|
||||
|
||||
func Markdownify(m map[string]*RawDocumentation) map[string]*Documentation {
|
||||
out := make(map[string]*Documentation, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = &Documentation{
|
||||
Title: strings.TrimSpace(stripMarkdown(v.Title)),
|
||||
Text: strings.TrimSpace(stripMarkdown(v.Text)),
|
||||
|
||||
TitleMarkdown: strings.TrimSpace(toMarkdown(v.Title)),
|
||||
TextMarkdown: strings.TrimSpace(toMarkdown(v.Text)),
|
||||
|
||||
Before: strings.TrimSpace(v.Before),
|
||||
After: strings.TrimSpace(v.After),
|
||||
Since: v.Since,
|
||||
NonDefault: v.NonDefault,
|
||||
Options: v.Options,
|
||||
Severity: v.Severity,
|
||||
MergeIf: v.MergeIf,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toMarkdown(s string) string {
|
||||
return strings.NewReplacer(`\'`, "`", `\"`, "`").Replace(s)
|
||||
}
|
||||
|
||||
func stripMarkdown(s string) string {
|
||||
return strings.NewReplacer(`\'`, "", `\"`, "'").Replace(s)
|
||||
}
|
||||
|
||||
func (doc *Documentation) Format(metadata bool) string {
|
||||
return doc.format(false, metadata)
|
||||
}
|
||||
|
||||
func (doc *Documentation) FormatMarkdown(metadata bool) string {
|
||||
return doc.format(true, metadata)
|
||||
}
|
||||
|
||||
func (doc *Documentation) format(markdown bool, metadata bool) string {
|
||||
b := &strings.Builder{}
|
||||
if markdown {
|
||||
fmt.Fprintf(b, "%s\n\n", doc.TitleMarkdown)
|
||||
if doc.Text != "" {
|
||||
fmt.Fprintf(b, "%s\n\n", doc.TextMarkdown)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s\n\n", doc.Title)
|
||||
if doc.Text != "" {
|
||||
fmt.Fprintf(b, "%s\n\n", doc.Text)
|
||||
}
|
||||
}
|
||||
|
||||
if doc.Before != "" {
|
||||
fmt.Fprintln(b, "Before:")
|
||||
fmt.Fprintln(b, "")
|
||||
for _, line := range strings.Split(doc.Before, "\n") {
|
||||
fmt.Fprint(b, " ", line, "\n")
|
||||
}
|
||||
fmt.Fprintln(b, "")
|
||||
fmt.Fprintln(b, "After:")
|
||||
fmt.Fprintln(b, "")
|
||||
for _, line := range strings.Split(doc.After, "\n") {
|
||||
fmt.Fprint(b, " ", line, "\n")
|
||||
}
|
||||
fmt.Fprintln(b, "")
|
||||
}
|
||||
|
||||
if metadata {
|
||||
fmt.Fprint(b, "Available since\n ")
|
||||
if doc.Since == "" {
|
||||
fmt.Fprint(b, "unreleased")
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s", doc.Since)
|
||||
}
|
||||
if doc.NonDefault {
|
||||
fmt.Fprint(b, ", non-default")
|
||||
}
|
||||
fmt.Fprint(b, "\n")
|
||||
if len(doc.Options) > 0 {
|
||||
fmt.Fprintf(b, "\nOptions\n")
|
||||
for _, opt := range doc.Options {
|
||||
fmt.Fprintf(b, " %s", opt)
|
||||
}
|
||||
fmt.Fprint(b, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (doc *Documentation) String() string {
|
||||
return doc.Format(true)
|
||||
}
|
||||
|
||||
func newVersionFlag() flag.Getter {
|
||||
tags := build.Default.ReleaseTags
|
||||
v := tags[len(tags)-1][2:]
|
||||
version := new(VersionFlag)
|
||||
if err := version.Set(v); err != nil {
|
||||
panic(fmt.Sprintf("internal error: %s", err))
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
type VersionFlag int
|
||||
|
||||
func (v *VersionFlag) String() string {
|
||||
return fmt.Sprintf("1.%d", *v)
|
||||
}
|
||||
|
||||
var goVersionRE = regexp.MustCompile(`^(?:go)?1.(\d+).*$`)
|
||||
|
||||
// ParseGoVersion parses Go versions of the form 1.M, 1.M.N, or 1.M.NrcR, with an optional "go" prefix. It assumes that
|
||||
// versions have already been verified and are valid.
|
||||
func ParseGoVersion(s string) (int, bool) {
|
||||
m := goVersionRE.FindStringSubmatch(s)
|
||||
if m == nil {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
func (v *VersionFlag) Set(s string) error {
|
||||
n, ok := ParseGoVersion(s)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Go version: %q", s)
|
||||
}
|
||||
*v = VersionFlag(n)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VersionFlag) Get() interface{} {
|
||||
return int(*v)
|
||||
}
|
||||
|
||||
// ExhaustiveTypeSwitch panics when called. It can be used to ensure
|
||||
// that type switches are exhaustive.
|
||||
func ExhaustiveTypeSwitch(v interface{}) {
|
||||
panic(fmt.Sprintf("internal error: unhandled case %T", v))
|
||||
}
|
||||
|
||||
// A directive is a comment of the form '//lint:<command>
|
||||
// [arguments...]'. It represents instructions to the static analysis
|
||||
// tool.
|
||||
type Directive struct {
|
||||
Command string
|
||||
Arguments []string
|
||||
Directive *ast.Comment
|
||||
Node ast.Node
|
||||
}
|
||||
|
||||
func parseDirective(s string) (cmd string, args []string) {
|
||||
if !strings.HasPrefix(s, "//lint:") {
|
||||
return "", nil
|
||||
}
|
||||
s = strings.TrimPrefix(s, "//lint:")
|
||||
fields := strings.Split(s, " ")
|
||||
return fields[0], fields[1:]
|
||||
}
|
||||
|
||||
// ParseDirectives extracts all directives from a list of Go files.
|
||||
func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
|
||||
var dirs []Directive
|
||||
for _, f := range files {
|
||||
// OPT(dh): in our old code, we skip all the comment map work if we
|
||||
// couldn't find any directives, benchmark if that's actually
|
||||
// worth doing
|
||||
cm := ast.NewCommentMap(fset, f, f.Comments)
|
||||
for node, cgs := range cm {
|
||||
for _, cg := range cgs {
|
||||
for _, c := range cg.List {
|
||||
if !strings.HasPrefix(c.Text, "//lint:") {
|
||||
continue
|
||||
}
|
||||
cmd, args := parseDirective(c.Text)
|
||||
d := Directive{
|
||||
Command: cmd,
|
||||
Arguments: args,
|
||||
Directive: c,
|
||||
Node: node,
|
||||
}
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file is a modified copy of x/tools/go/analysis/analysistest/analysistest.go
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/internal/diff/myers"
|
||||
"honnef.co/go/tools/lintcmd/runner"
|
||||
|
||||
"golang.org/x/tools/go/expect"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
func CheckSuggestedFixes(t *testing.T, diagnostics []runner.Diagnostic) {
|
||||
// Process each result (package) separately, matching up the suggested
|
||||
// fixes into a diff, which we will compare to the .golden file. We have
|
||||
// to do this per-result in case a file appears in two packages, such as in
|
||||
// packages with tests, where mypkg/a.go will appear in both mypkg and
|
||||
// mypkg.test. In that case, the analyzer may suggest the same set of
|
||||
// changes to a.go for each package. If we merge all the results, those
|
||||
// changes get doubly applied, which will cause conflicts or mismatches.
|
||||
// Validating the results separately means as long as the two analyses
|
||||
// don't produce conflicting suggestions for a single file, everything
|
||||
// should match up.
|
||||
// file -> message -> edits
|
||||
fileEdits := make(map[string]map[string][]runner.TextEdit)
|
||||
fileContents := make(map[string][]byte)
|
||||
|
||||
// Validate edits, prepare the fileEdits map and read the file contents.
|
||||
for _, diag := range diagnostics {
|
||||
for _, sf := range diag.SuggestedFixes {
|
||||
for _, edit := range sf.TextEdits {
|
||||
// Validate the edit.
|
||||
if edit.Position.Offset > edit.End.Offset {
|
||||
t.Errorf(
|
||||
"diagnostic for analysis %v contains Suggested Fix with malformed edit: pos (%v) > end (%v)",
|
||||
diag.Category, edit.Position.Offset, edit.End.Offset)
|
||||
continue
|
||||
}
|
||||
if edit.Position.Filename != edit.End.Filename {
|
||||
t.Errorf(
|
||||
"diagnostic for analysis %v contains Suggested Fix with malformed edit spanning files %v and %v",
|
||||
diag.Category, edit.Position.Filename, edit.End.Filename)
|
||||
continue
|
||||
}
|
||||
if _, ok := fileContents[edit.Position.Filename]; !ok {
|
||||
contents, err := os.ReadFile(edit.Position.Filename)
|
||||
if err != nil {
|
||||
t.Errorf("error reading %s: %v", edit.Position.Filename, err)
|
||||
}
|
||||
fileContents[edit.Position.Filename] = contents
|
||||
}
|
||||
|
||||
if _, ok := fileEdits[edit.Position.Filename]; !ok {
|
||||
fileEdits[edit.Position.Filename] = make(map[string][]runner.TextEdit)
|
||||
}
|
||||
fileEdits[edit.Position.Filename][sf.Message] = append(fileEdits[edit.Position.Filename][sf.Message], edit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for file, fixes := range fileEdits {
|
||||
// Get the original file contents.
|
||||
orig, ok := fileContents[file]
|
||||
if !ok {
|
||||
t.Errorf("could not find file contents for %s", file)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the golden file and read the contents.
|
||||
ar, err := txtar.ParseFile(file + ".golden")
|
||||
if err != nil {
|
||||
t.Errorf("error reading %s.golden: %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(ar.Files) > 0 {
|
||||
// one virtual file per kind of suggested fix
|
||||
|
||||
if len(ar.Comment) != 0 {
|
||||
// we allow either just the comment, or just virtual
|
||||
// files, not both. it is not clear how "both" should
|
||||
// behave.
|
||||
t.Errorf("%s.golden has leading comment; we don't know what to do with it", file)
|
||||
continue
|
||||
}
|
||||
|
||||
var sfs []string
|
||||
for sf := range fixes {
|
||||
sfs = append(sfs, sf)
|
||||
}
|
||||
sort.Slice(sfs, func(i, j int) bool {
|
||||
return sfs[i] < sfs[j]
|
||||
})
|
||||
for _, sf := range sfs {
|
||||
edits := fixes[sf]
|
||||
found := false
|
||||
for _, vf := range ar.Files {
|
||||
if vf.Name == sf {
|
||||
found = true
|
||||
out := applyEdits(orig, edits)
|
||||
// the file may contain multiple trailing
|
||||
// newlines if the user places empty lines
|
||||
// between files in the archive. normalize
|
||||
// this to a single newline.
|
||||
want := string(bytes.TrimRight(vf.Data, "\n")) + "\n"
|
||||
formatted, err := format.Source([]byte(out))
|
||||
if err != nil {
|
||||
t.Errorf("%s: error formatting edited source: %v\n%s", file, err, out)
|
||||
continue
|
||||
}
|
||||
if want != string(formatted) {
|
||||
d := myers.ComputeEdits(want, string(formatted))
|
||||
diff := ""
|
||||
for _, op := range d {
|
||||
diff += op.String()
|
||||
}
|
||||
t.Errorf("suggested fixes failed for %s[%s]:\n%s", file, sf, diff)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("no section for suggested fix %q in %s.golden", sf, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// all suggested fixes are represented by a single file
|
||||
|
||||
var catchallEdits []runner.TextEdit
|
||||
for _, edits := range fixes {
|
||||
catchallEdits = append(catchallEdits, edits...)
|
||||
}
|
||||
|
||||
out := applyEdits(orig, catchallEdits)
|
||||
want := string(ar.Comment)
|
||||
|
||||
formatted, err := format.Source([]byte(out))
|
||||
if err != nil {
|
||||
t.Errorf("%s: error formatting resulting source: %v\n%s", file, err, out)
|
||||
continue
|
||||
}
|
||||
if want != string(formatted) {
|
||||
d := myers.ComputeEdits(want, string(formatted))
|
||||
diff := ""
|
||||
for _, op := range d {
|
||||
diff += op.String()
|
||||
}
|
||||
t.Errorf("suggested fixes failed for %s:\n%s", file, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Check(t *testing.T, gopath string, files []string, diagnostics []runner.Diagnostic, facts []runner.TestFact) {
|
||||
relativePath := func(path string) string {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
rel, err := filepath.Rel(cwd, path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
type key struct {
|
||||
file string
|
||||
line int
|
||||
}
|
||||
|
||||
// the 'files' argument contains a list of all files that were part of the tested package
|
||||
want := make(map[key][]*expect.Note)
|
||||
|
||||
fset := token.NewFileSet()
|
||||
seen := map[string]struct{}{}
|
||||
for _, file := range files {
|
||||
seen[file] = struct{}{}
|
||||
|
||||
notes, err := expect.Parse(fset, file, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, note := range notes {
|
||||
k := key{
|
||||
file: file,
|
||||
line: fset.PositionFor(note.Pos, false).Line,
|
||||
}
|
||||
want[k] = append(want[k], note)
|
||||
}
|
||||
}
|
||||
|
||||
for _, diag := range diagnostics {
|
||||
file := diag.Position.Filename
|
||||
if _, ok := seen[file]; !ok {
|
||||
t.Errorf("got diagnostic in file %q, but that file isn't part of the checked package", relativePath(file))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
check := func(posn token.Position, message string, kind string, argIdx int, identifier string) {
|
||||
k := key{posn.Filename, posn.Line}
|
||||
expects := want[k]
|
||||
var unmatched []string
|
||||
for i, exp := range expects {
|
||||
if exp.Name == kind {
|
||||
if kind == "fact" && exp.Args[0] != expect.Identifier(identifier) {
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
switch arg := exp.Args[argIdx].(type) {
|
||||
case string:
|
||||
matched = strings.Contains(message, arg)
|
||||
case *regexp.Regexp:
|
||||
matched = arg.MatchString(message)
|
||||
default:
|
||||
t.Fatalf("unexpected argument type %T", arg)
|
||||
}
|
||||
if matched {
|
||||
// matched: remove the expectation.
|
||||
expects[i] = expects[len(expects)-1]
|
||||
expects = expects[:len(expects)-1]
|
||||
want[k] = expects
|
||||
return
|
||||
}
|
||||
unmatched = append(unmatched, fmt.Sprintf("%q", exp.Args[argIdx]))
|
||||
}
|
||||
}
|
||||
if unmatched == nil {
|
||||
posn.Filename = relativePath(posn.Filename)
|
||||
t.Errorf("%v: unexpected diag: %v", posn, message)
|
||||
} else {
|
||||
posn.Filename = relativePath(posn.Filename)
|
||||
t.Errorf("%v: diag %q does not match pattern %s",
|
||||
posn, message, strings.Join(unmatched, " or "))
|
||||
}
|
||||
}
|
||||
|
||||
checkDiag := func(posn token.Position, message string) {
|
||||
check(posn, message, "diag", 0, "")
|
||||
}
|
||||
|
||||
checkFact := func(posn token.Position, name, message string) {
|
||||
check(posn, message, "fact", 1, name)
|
||||
}
|
||||
|
||||
// Check the diagnostics match expectations.
|
||||
for _, f := range diagnostics {
|
||||
// TODO(matloob): Support ranges in analysistest.
|
||||
posn := f.Position
|
||||
checkDiag(posn, f.Message)
|
||||
}
|
||||
|
||||
// Check the facts match expectations.
|
||||
for _, fact := range facts {
|
||||
name := fact.ObjectName
|
||||
posn := fact.Position
|
||||
if name == "" {
|
||||
name = "package"
|
||||
posn.Line = 1
|
||||
}
|
||||
|
||||
checkFact(posn, name, fact.FactString)
|
||||
}
|
||||
|
||||
// Reject surplus expectations.
|
||||
//
|
||||
// Sometimes an Analyzer reports two similar diagnostics on a
|
||||
// line with only one expectation. The reader may be confused by
|
||||
// the error message.
|
||||
// TODO(adonovan): print a better error:
|
||||
// "got 2 diagnostics here; each one needs its own expectation".
|
||||
var surplus []string
|
||||
for key, expects := range want {
|
||||
for _, exp := range expects {
|
||||
surplus = append(surplus, fmt.Sprintf("%s:%d: no %s was reported matching %q", relativePath(key.file), key.line, exp.Name, exp.Args))
|
||||
}
|
||||
}
|
||||
sort.Strings(surplus)
|
||||
for _, err := range surplus {
|
||||
t.Errorf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func applyEdits(src []byte, edits []runner.TextEdit) []byte {
|
||||
// This function isn't efficient, but it doesn't have to be.
|
||||
|
||||
edits = append([]runner.TextEdit(nil), edits...)
|
||||
sort.Slice(edits, func(i, j int) bool {
|
||||
if edits[i].Position.Offset < edits[j].Position.Offset {
|
||||
return true
|
||||
}
|
||||
if edits[i].Position.Offset == edits[j].Position.Offset {
|
||||
return edits[i].End.Offset < edits[j].End.Offset
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
out := append([]byte(nil), src...)
|
||||
offset := 0
|
||||
for _, edit := range edits {
|
||||
start := edit.Position.Offset + offset
|
||||
end := edit.End.Offset + offset
|
||||
if edit.End == (token.Position{}) {
|
||||
end = -1
|
||||
}
|
||||
if len(edit.NewText) == 0 {
|
||||
// pure deletion
|
||||
copy(out[start:], out[end:])
|
||||
out = out[:len(out)-(end-start)]
|
||||
offset -= end - start
|
||||
} else if end == -1 || end == start {
|
||||
// pure insertion
|
||||
tmp := make([]byte, len(out)+len(edit.NewText))
|
||||
copy(tmp, out[:start])
|
||||
copy(tmp[start:], edit.NewText)
|
||||
copy(tmp[start+len(edit.NewText):], out[start:])
|
||||
offset += len(edit.NewText)
|
||||
out = tmp
|
||||
} else if end-start == len(edit.NewText) {
|
||||
// exact replacement
|
||||
copy(out[start:], edit.NewText)
|
||||
} else if end-start < len(edit.NewText) {
|
||||
// replace with longer string
|
||||
growth := len(edit.NewText) - (end - start)
|
||||
tmp := make([]byte, len(out)+growth)
|
||||
copy(tmp, out[:start])
|
||||
copy(tmp[start:], edit.NewText)
|
||||
copy(tmp[start+len(edit.NewText):], out[end:])
|
||||
offset += growth
|
||||
out = tmp
|
||||
} else if end-start > len(edit.NewText) {
|
||||
// replace with shorter string
|
||||
shrinkage := (end - start) - len(edit.NewText)
|
||||
|
||||
copy(out[start:], edit.NewText)
|
||||
copy(out[start+len(edit.NewText):], out[end:])
|
||||
out = out[:len(out)-shrinkage]
|
||||
offset -= shrinkage
|
||||
}
|
||||
}
|
||||
|
||||
// Debug code
|
||||
if false {
|
||||
fmt.Println("input:")
|
||||
fmt.Println(string(src))
|
||||
fmt.Println()
|
||||
fmt.Println("edits:")
|
||||
for _, edit := range edits {
|
||||
fmt.Printf("%d:%d - %d:%d <- %q\n", edit.Position.Line, edit.Position.Column, edit.End.Line, edit.End.Column, edit.NewText)
|
||||
}
|
||||
fmt.Println("output:")
|
||||
fmt.Println(string(out))
|
||||
panic("")
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"go/build"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/analysis/lint"
|
||||
"honnef.co/go/tools/config"
|
||||
"honnef.co/go/tools/go/buildid"
|
||||
"honnef.co/go/tools/lintcmd/cache"
|
||||
"honnef.co/go/tools/lintcmd/runner"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
type Test struct {
|
||||
Dir string
|
||||
Version string
|
||||
}
|
||||
|
||||
func computeSalt() ([]byte, error) {
|
||||
p, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id, err := buildid.ReadFile(p); err == nil {
|
||||
return []byte(id), nil
|
||||
} else {
|
||||
// For some reason we couldn't read the build id from the executable.
|
||||
// Fall back to hashing the entire executable.
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
}
|
||||
|
||||
func defaultGoVersion() string {
|
||||
tags := build.Default.ReleaseTags
|
||||
v := tags[len(tags)-1][2:]
|
||||
return v
|
||||
}
|
||||
|
||||
func Run(t *testing.T, analyzers []*lint.Analyzer, tests map[string][]Test) {
|
||||
analyzersByName := map[string]*lint.Analyzer{}
|
||||
for _, a := range analyzers {
|
||||
analyzersByName[a.Analyzer.Name] = a
|
||||
}
|
||||
|
||||
analyzersByVersion := map[string]map[*lint.Analyzer]struct{}{}
|
||||
dirsByVersion := map[string][]string{}
|
||||
|
||||
for analyzerName, ttt := range tests {
|
||||
for _, tt := range ttt {
|
||||
m := analyzersByVersion[tt.Version]
|
||||
if m == nil {
|
||||
m = map[*lint.Analyzer]struct{}{}
|
||||
analyzersByVersion[tt.Version] = m
|
||||
}
|
||||
|
||||
analyzer, ok := analyzersByName[analyzerName]
|
||||
if !ok {
|
||||
t.Errorf("found tests for analyzer %q, but no such analyzer exists", analyzerName)
|
||||
continue
|
||||
}
|
||||
m[analyzer] = struct{}{}
|
||||
|
||||
dirsByVersion[tt.Version] = append(dirsByVersion[tt.Version], tt.Dir)
|
||||
}
|
||||
}
|
||||
|
||||
for v, asm := range analyzersByVersion {
|
||||
dirs := dirsByVersion[v]
|
||||
|
||||
actualVersion := v
|
||||
if actualVersion == "" {
|
||||
actualVersion = defaultGoVersion()
|
||||
}
|
||||
as := make([]*analysis.Analyzer, 0, len(asm))
|
||||
for a := range asm {
|
||||
as = append(as, a.Analyzer)
|
||||
if err := a.Analyzer.Flags.Lookup("go").Value.Set(actualVersion); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := cache.Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
salt, err := computeSalt()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.SetSalt(salt)
|
||||
r, err := runner.New(config.Config{}, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.GoVersion = actualVersion
|
||||
r.TestMode = true
|
||||
|
||||
testdata, err := filepath.Abs("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &packages.Config{
|
||||
Tests: true,
|
||||
Env: append(os.Environ(), "GOPATH="+testdata, "GO111MODULE=off", "GOPROXY=off"),
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
t.Fatal("no directories for version", v)
|
||||
}
|
||||
res, err := r.Run(cfg, as, dirs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Each result in res contains all diagnostics and facts for all checked packages for all checked analyzers.
|
||||
// For each package, we only care about the diagnostics and facts reported by a single analyzer.
|
||||
|
||||
// resultByPath maps from import path to results
|
||||
resultByPath := map[string][]runner.Result{}
|
||||
failed := false
|
||||
for _, r := range res {
|
||||
if r.Failed {
|
||||
failed = true
|
||||
if len(r.Errors) > 0 {
|
||||
t.Fatalf("failed checking %s: %v", r.Package.PkgPath, r.Errors)
|
||||
}
|
||||
}
|
||||
// r.Package.PkgPath is not unique. The same path can refer to a package and a package plus its
|
||||
// (non-external) tests.
|
||||
resultByPath[r.Package.PkgPath] = append(resultByPath[r.Package.PkgPath], r)
|
||||
}
|
||||
|
||||
if failed {
|
||||
t.Fatal("failed processing package, but got no errors")
|
||||
}
|
||||
|
||||
for a, ttt := range tests {
|
||||
for _, tt := range ttt {
|
||||
if tt.Version != v {
|
||||
continue
|
||||
}
|
||||
any := false
|
||||
for _, suffix := range []string{"", ".test", "_test"} {
|
||||
dir := tt.Dir + suffix
|
||||
rr, ok := resultByPath[dir]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
// Remove this result. We later check that there remain no tests we haven't checked.
|
||||
delete(resultByPath, dir)
|
||||
|
||||
for _, r := range rr {
|
||||
data, err := r.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tdata, err := r.LoadTest()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Select those diagnostics made by the analyzer we're currently checking
|
||||
var relevantDiags []runner.Diagnostic
|
||||
for _, diag := range data.Diagnostics {
|
||||
// FIXME(dh): Category might not match analyzer names. it does for Staticcheck, for now
|
||||
if diag.Category != a {
|
||||
continue
|
||||
}
|
||||
relevantDiags = append(relevantDiags, diag)
|
||||
}
|
||||
|
||||
var relevantFacts []runner.TestFact
|
||||
for _, fact := range tdata.Facts {
|
||||
if fact.Analyzer != a {
|
||||
continue
|
||||
}
|
||||
relevantFacts = append(relevantFacts, fact)
|
||||
}
|
||||
|
||||
Check(t, testdata, tdata.Files, relevantDiags, relevantFacts)
|
||||
CheckSuggestedFixes(t, relevantDiags)
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
t.Errorf("no result for directory %s", tt.Dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, rr := range resultByPath {
|
||||
for _, r := range rr {
|
||||
data, err := r.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(data.Diagnostics) != 0 {
|
||||
t.Errorf("unexpected diagnostics in package %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/analysis/facts/generated"
|
||||
"honnef.co/go/tools/go/ast/astutil"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ShortRange bool
|
||||
FilterGenerated bool
|
||||
Fixes []analysis.SuggestedFix
|
||||
Related []analysis.RelatedInformation
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func ShortRange() Option {
|
||||
return func(opts *Options) {
|
||||
opts.ShortRange = true
|
||||
}
|
||||
}
|
||||
|
||||
func FilterGenerated() Option {
|
||||
return func(opts *Options) {
|
||||
opts.FilterGenerated = true
|
||||
}
|
||||
}
|
||||
|
||||
func Fixes(fixes ...analysis.SuggestedFix) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Fixes = append(opts.Fixes, fixes...)
|
||||
}
|
||||
}
|
||||
|
||||
func Related(node Positioner, message string) Option {
|
||||
return func(opts *Options) {
|
||||
pos, end, ok := getRange(node, opts.ShortRange)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r := analysis.RelatedInformation{
|
||||
Pos: pos,
|
||||
End: end,
|
||||
Message: message,
|
||||
}
|
||||
opts.Related = append(opts.Related, r)
|
||||
}
|
||||
}
|
||||
|
||||
type Positioner interface {
|
||||
Pos() token.Pos
|
||||
}
|
||||
|
||||
type fullPositioner interface {
|
||||
Pos() token.Pos
|
||||
End() token.Pos
|
||||
}
|
||||
|
||||
type sourcer interface {
|
||||
Source() ast.Node
|
||||
}
|
||||
|
||||
// shortRange returns the position and end of the main component of an
|
||||
// AST node. For nodes that have no body, the short range is identical
|
||||
// to the node's Pos and End. For nodes that do have a body, the short
|
||||
// range excludes the body.
|
||||
func shortRange(node ast.Node) (pos, end token.Pos) {
|
||||
switch node := node.(type) {
|
||||
case *ast.File:
|
||||
return node.Pos(), node.Name.End()
|
||||
case *ast.CaseClause:
|
||||
return node.Pos(), node.Colon + 1
|
||||
case *ast.CommClause:
|
||||
return node.Pos(), node.Colon + 1
|
||||
case *ast.DeferStmt:
|
||||
return node.Pos(), node.Defer + token.Pos(len("defer"))
|
||||
case *ast.ExprStmt:
|
||||
return shortRange(node.X)
|
||||
case *ast.ForStmt:
|
||||
if node.Post != nil {
|
||||
return node.For, node.Post.End()
|
||||
} else if node.Cond != nil {
|
||||
return node.For, node.Cond.End()
|
||||
} else if node.Init != nil {
|
||||
// +1 to catch the semicolon, for gofmt'ed code
|
||||
return node.Pos(), node.Init.End() + 1
|
||||
} else {
|
||||
return node.Pos(), node.For + token.Pos(len("for"))
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
return node.Pos(), node.Type.End()
|
||||
case *ast.FuncLit:
|
||||
return node.Pos(), node.Type.End()
|
||||
case *ast.GoStmt:
|
||||
if _, ok := astutil.Unparen(node.Call.Fun).(*ast.FuncLit); ok {
|
||||
return node.Pos(), node.Go + token.Pos(len("go"))
|
||||
} else {
|
||||
return node.Pos(), node.End()
|
||||
}
|
||||
case *ast.IfStmt:
|
||||
return node.Pos(), node.Cond.End()
|
||||
case *ast.RangeStmt:
|
||||
return node.Pos(), node.X.End()
|
||||
case *ast.SelectStmt:
|
||||
return node.Pos(), node.Pos() + token.Pos(len("select"))
|
||||
case *ast.SwitchStmt:
|
||||
if node.Tag != nil {
|
||||
return node.Pos(), node.Tag.End()
|
||||
} else if node.Init != nil {
|
||||
// +1 to catch the semicolon, for gofmt'ed code
|
||||
return node.Pos(), node.Init.End() + 1
|
||||
} else {
|
||||
return node.Pos(), node.Pos() + token.Pos(len("switch"))
|
||||
}
|
||||
case *ast.TypeSwitchStmt:
|
||||
return node.Pos(), node.Assign.End()
|
||||
default:
|
||||
return node.Pos(), node.End()
|
||||
}
|
||||
}
|
||||
|
||||
func HasRange(node Positioner) bool {
|
||||
// we don't know if getRange will be called with shortRange set to
|
||||
// true, so make sure that both work.
|
||||
_, _, ok := getRange(node, false)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, _, ok = getRange(node, true)
|
||||
return ok
|
||||
}
|
||||
|
||||
func getRange(node Positioner, short bool) (pos, end token.Pos, ok bool) {
|
||||
switch n := node.(type) {
|
||||
case sourcer:
|
||||
s := n.Source()
|
||||
if s == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
if short {
|
||||
p, e := shortRange(s)
|
||||
return p, e, true
|
||||
}
|
||||
return s.Pos(), s.End(), true
|
||||
case fullPositioner:
|
||||
if short {
|
||||
p, e := shortRange(n)
|
||||
return p, e, true
|
||||
}
|
||||
return n.Pos(), n.End(), true
|
||||
default:
|
||||
return n.Pos(), token.NoPos, true
|
||||
}
|
||||
}
|
||||
|
||||
func Report(pass *analysis.Pass, node Positioner, message string, opts ...Option) {
|
||||
cfg := &Options{}
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
file := DisplayPosition(pass.Fset, node.Pos()).Filename
|
||||
if cfg.FilterGenerated {
|
||||
m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator)
|
||||
if _, ok := m[file]; ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pos, end, ok := getRange(node, cfg.ShortRange)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("no valid position for reporting node %v", node))
|
||||
}
|
||||
d := analysis.Diagnostic{
|
||||
Pos: pos,
|
||||
End: end,
|
||||
Message: message,
|
||||
SuggestedFixes: cfg.Fixes,
|
||||
Related: cfg.Related,
|
||||
}
|
||||
pass.Report(d)
|
||||
}
|
||||
|
||||
func Render(pass *analysis.Pass, x interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
if err := format.Node(&buf, pass.Fset, x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func RenderArgs(pass *analysis.Pass, args []ast.Expr) string {
|
||||
var ss []string
|
||||
for _, arg := range args {
|
||||
ss = append(ss, Render(pass, arg))
|
||||
}
|
||||
return strings.Join(ss, ", ")
|
||||
}
|
||||
|
||||
func DisplayPosition(fset *token.FileSet, p token.Pos) token.Position {
|
||||
if p == token.NoPos {
|
||||
return token.Position{}
|
||||
}
|
||||
|
||||
// Only use the adjusted position if it points to another Go file.
|
||||
// This means we'll point to the original file for cgo files, but
|
||||
// we won't point to a YACC grammar file.
|
||||
pos := fset.PositionFor(p, false)
|
||||
adjPos := fset.PositionFor(p, true)
|
||||
|
||||
if filepath.Ext(adjPos.Filename) == ".go" {
|
||||
return adjPos
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func Ordinal(n int) string {
|
||||
suffix := "th"
|
||||
if n < 10 || n > 20 {
|
||||
switch n % 10 {
|
||||
case 0:
|
||||
suffix = "th"
|
||||
case 1:
|
||||
suffix = "st"
|
||||
case 2:
|
||||
suffix = "nd"
|
||||
case 3:
|
||||
suffix = "rd"
|
||||
default:
|
||||
suffix = "th"
|
||||
}
|
||||
}
|
||||
|
||||
return strconv.Itoa(n) + suffix
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package report
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOrdinal(t *testing.T) {
|
||||
tests := []struct {
|
||||
num int
|
||||
want string
|
||||
}{
|
||||
{0, "0th"}, {1, "1st"}, {2, "2nd"}, {3, "3rd"}, {4, "4th"}, {5, "5th"}, {6, "6th"}, {7, "7th"}, {8, "8th"}, {9, "9th"},
|
||||
{10, "10th"}, {11, "11th"}, {12, "12th"}, {13, "13th"}, {14, "14th"}, {15, "15th"}, {16, "16th"}, {17, "17th"}, {18, "18th"}, {19, "19th"},
|
||||
{20, "20th"}, {21, "21st"}, {22, "22nd"}, {23, "23rd"}, {24, "24th"}, {25, "25th"}, {26, "26th"}, {27, "27th"}, {28, "28th"}, {29, "29th"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := Ordinal(tt.num); got != tt.want {
|
||||
t.Errorf("Ordinal(%d) = %s, want %s", tt.num, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
Keyify turns unkeyed struct literals (`T{1, 2, 3}`) into keyed
|
||||
ones (`T{A: 1, B: 2, C: 3}`)
|
||||
|
||||
## Installation
|
||||
|
||||
See [the main README](https://github.com/dominikh/go-tools#installation) for installation instructions.
|
||||
|
||||
## Usage
|
||||
|
||||
Call keyify with a position such as `/some/file.go:#5`, where #5 is
|
||||
the byte offset in the file and has to point at or into a struct
|
||||
literal.
|
||||
|
||||
By default, keyify will print the new literal on stdout, formatted as
|
||||
Go code. By using the `-json` flag, it will print a JSON object
|
||||
denoting the start and end of the original literal, and its
|
||||
replacement. This is useful for integration with editors.
|
||||
|
||||
For a description of all available flags, see `keyify -help`.
|
||||
|
||||
### Emacs
|
||||
|
||||
For Emacs integration, add the following to your `.emacs` file:
|
||||
|
||||
```
|
||||
(add-to-list 'load-path "/your/gopath/src/honnef.co/go/tools/cmd/keyify)
|
||||
(eval-after-load 'go-mode
|
||||
(lambda ()
|
||||
(require 'go-keyify)))
|
||||
```
|
||||
|
||||
With point on or in a struct literal, call the `go-keyify` command.
|
||||
|
||||

|
||||
@@ -0,0 +1,51 @@
|
||||
;;; go-keyify.el --- keyify integration for Emacs
|
||||
|
||||
;; Copyright 2016 Dominik Honnef. All rights reserved.
|
||||
;; Use of this source code is governed by a BSD-style
|
||||
;; license that can be found in the LICENSE file.
|
||||
|
||||
;; Author: Dominik Honnef
|
||||
;; Version: 1.0.0
|
||||
;; Keywords: languages go
|
||||
;; URL: https://github.com/dominikh/go-keyify
|
||||
;;
|
||||
;; This file is not part of GNU Emacs.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'json)
|
||||
|
||||
;;;###autoload
|
||||
(defun go-keyify ()
|
||||
"Turn an unkeyed struct literal into a keyed one.
|
||||
|
||||
Call with point on or in a struct literal."
|
||||
(interactive)
|
||||
(let* ((name (buffer-file-name))
|
||||
(point (point))
|
||||
(bpoint (1- (position-bytes point)))
|
||||
(out (get-buffer-create "*go-keyify-output")))
|
||||
(with-current-buffer out
|
||||
(setq buffer-read-only nil)
|
||||
(erase-buffer))
|
||||
(with-current-buffer (get-buffer-create "*go-keyify-input*")
|
||||
(setq buffer-read-only nil)
|
||||
(erase-buffer)
|
||||
(go--insert-modified-files)
|
||||
(call-process-region (point-min) (point-max) "keyify" t out nil
|
||||
"-modified"
|
||||
"-json"
|
||||
(format "%s:#%d" name bpoint)))
|
||||
(let ((res (with-current-buffer out
|
||||
(goto-char (point-min))
|
||||
(json-read))))
|
||||
(delete-region
|
||||
(1+ (cdr (assoc 'start res)))
|
||||
(1+ (cdr (assoc 'end res))))
|
||||
(insert (cdr (assoc 'replacement res)))
|
||||
(indent-region (1+ (cdr (assoc 'start res))) (point))
|
||||
(goto-char point))))
|
||||
|
||||
(provide 'go-keyify)
|
||||
|
||||
;;; go-keyify.el ends here
|
||||
@@ -0,0 +1,411 @@
|
||||
// keyify transforms unkeyed struct literals into a keyed ones.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/constant"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"honnef.co/go/tools/go/ast/astutil"
|
||||
"honnef.co/go/tools/lintcmd/version"
|
||||
|
||||
"golang.org/x/tools/go/buildutil"
|
||||
|
||||
//lint:ignore SA1019 this tool is unmaintained, just keep it working
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
var (
|
||||
fRecursive bool
|
||||
fOneLine bool
|
||||
fJSON bool
|
||||
fMinify bool
|
||||
fModified bool
|
||||
fVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&fRecursive, "r", false, "keyify struct initializers recursively")
|
||||
flag.BoolVar(&fOneLine, "o", false, "print new struct initializer on a single line")
|
||||
flag.BoolVar(&fJSON, "json", false, "print new struct initializer as JSON")
|
||||
flag.BoolVar(&fMinify, "m", false, "omit fields that are set to their zero value")
|
||||
flag.BoolVar(&fModified, "modified", false, "read an archive of modified files from standard input")
|
||||
flag.BoolVar(&fVersion, "version", false, "Print version and exit")
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Printf("Usage: %s [flags] <position>\n\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if fVersion {
|
||||
version.Print(version.Version, version.MachineVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
pos := flag.Args()[0]
|
||||
name, start, _, err := parsePos(pos)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
eval, err := filepath.EvalSymlinks(name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
name, err = filepath.Abs(eval)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ctx := &build.Default
|
||||
if fModified {
|
||||
overlay, err := buildutil.ParseOverlayArchive(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ctx = buildutil.OverlayContext(ctx, overlay)
|
||||
}
|
||||
bpkg, err := buildutil.ContainingPackage(ctx, cwd, name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
conf := &loader.Config{
|
||||
Build: ctx,
|
||||
}
|
||||
conf.TypeCheckFuncBodies = func(s string) bool {
|
||||
return s == bpkg.ImportPath || s == bpkg.ImportPath+"_test"
|
||||
}
|
||||
conf.ImportWithTests(bpkg.ImportPath)
|
||||
lprog, err := conf.Load()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var tf *token.File
|
||||
var af *ast.File
|
||||
var pkg *loader.PackageInfo
|
||||
outer:
|
||||
for _, pkg = range lprog.InitialPackages() {
|
||||
for _, ff := range pkg.Files {
|
||||
file := lprog.Fset.File(ff.Pos())
|
||||
if file.Name() == name {
|
||||
af = ff
|
||||
tf = file
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if tf == nil {
|
||||
log.Fatalf("couldn't find file %s", name)
|
||||
}
|
||||
tstart, tend, err := fileOffsetToPos(tf, start, start)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
path, _ := astutil.PathEnclosingInterval(af, tstart, tend)
|
||||
var complit *ast.CompositeLit
|
||||
for _, p := range path {
|
||||
if p, ok := p.(*ast.CompositeLit); ok {
|
||||
complit = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if complit == nil {
|
||||
log.Fatal("no composite literal found near point")
|
||||
}
|
||||
if len(complit.Elts) == 0 {
|
||||
printComplit(complit, complit, lprog.Fset, lprog.Fset)
|
||||
return
|
||||
}
|
||||
if _, ok := complit.Elts[0].(*ast.KeyValueExpr); ok {
|
||||
lit := complit
|
||||
if fOneLine {
|
||||
lit = copyExpr(complit, 1).(*ast.CompositeLit)
|
||||
}
|
||||
printComplit(complit, lit, lprog.Fset, lprog.Fset)
|
||||
return
|
||||
}
|
||||
_, ok := pkg.TypeOf(complit).Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
log.Fatal("not a struct initialiser")
|
||||
return
|
||||
}
|
||||
|
||||
newComplit, lines := keyify(pkg, complit)
|
||||
newFset := token.NewFileSet()
|
||||
newFile := newFset.AddFile("", -1, lines)
|
||||
for i := 1; i <= lines; i++ {
|
||||
newFile.AddLine(i)
|
||||
}
|
||||
printComplit(complit, newComplit, lprog.Fset, newFset)
|
||||
}
|
||||
|
||||
func keyify(
|
||||
pkg *loader.PackageInfo,
|
||||
complit *ast.CompositeLit,
|
||||
) (*ast.CompositeLit, int) {
|
||||
var calcPos func(int) token.Pos
|
||||
if fOneLine {
|
||||
calcPos = func(int) token.Pos { return token.Pos(1) }
|
||||
} else {
|
||||
calcPos = func(i int) token.Pos { return token.Pos(2 + i) }
|
||||
}
|
||||
|
||||
st, _ := pkg.TypeOf(complit).Underlying().(*types.Struct)
|
||||
newComplit := &ast.CompositeLit{
|
||||
Type: complit.Type,
|
||||
Lbrace: 1,
|
||||
Rbrace: token.Pos(st.NumFields() + 2),
|
||||
}
|
||||
if fOneLine {
|
||||
newComplit.Rbrace = 1
|
||||
}
|
||||
numLines := 2 + st.NumFields()
|
||||
n := 0
|
||||
for i := 0; i < st.NumFields(); i++ {
|
||||
field := st.Field(i)
|
||||
val := complit.Elts[i]
|
||||
if fRecursive {
|
||||
if val2, ok := val.(*ast.CompositeLit); ok {
|
||||
if _, ok := pkg.TypeOf(val2.Type).Underlying().(*types.Struct); ok {
|
||||
// FIXME(dh): this code is obviously wrong. But
|
||||
// what were we intending to do here?
|
||||
var lines int
|
||||
numLines += lines
|
||||
//lint:ignore SA4006 See FIXME above.
|
||||
val, lines = keyify(pkg, val2)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, isIface := st.Field(i).Type().Underlying().(*types.Interface)
|
||||
if fMinify && (isNil(val, pkg) || (!isIface && isZero(val, pkg))) {
|
||||
continue
|
||||
}
|
||||
elt := &ast.KeyValueExpr{
|
||||
Key: &ast.Ident{NamePos: calcPos(n), Name: field.Name()},
|
||||
Value: copyExpr(val, calcPos(n)),
|
||||
}
|
||||
newComplit.Elts = append(newComplit.Elts, elt)
|
||||
n++
|
||||
}
|
||||
return newComplit, numLines
|
||||
}
|
||||
|
||||
func isNil(val ast.Expr, pkg *loader.PackageInfo) bool {
|
||||
ident, ok := val.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := pkg.ObjectOf(ident).(*types.Nil); ok {
|
||||
return true
|
||||
}
|
||||
if c, ok := pkg.ObjectOf(ident).(*types.Const); ok {
|
||||
if c.Val().Kind() != constant.Bool {
|
||||
return false
|
||||
}
|
||||
return !constant.BoolVal(c.Val())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isZero(val ast.Expr, pkg *loader.PackageInfo) bool {
|
||||
switch val := val.(type) {
|
||||
case *ast.BasicLit:
|
||||
switch val.Value {
|
||||
case `""`, "``", "0", "0.0", "0i", "0.":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case *ast.Ident:
|
||||
return isNil(val, pkg)
|
||||
case *ast.CompositeLit:
|
||||
typ := pkg.TypeOf(val.Type)
|
||||
if typ == nil {
|
||||
return false
|
||||
}
|
||||
isIface := false
|
||||
switch typ := typ.Underlying().(type) {
|
||||
case *types.Struct:
|
||||
case *types.Array:
|
||||
_, isIface = typ.Elem().Underlying().(*types.Interface)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
for _, elt := range val.Elts {
|
||||
if isNil(elt, pkg) || (!isIface && !isZero(elt, pkg)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func printComplit(oldlit, newlit *ast.CompositeLit, oldfset, newfset *token.FileSet) {
|
||||
buf := &bytes.Buffer{}
|
||||
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
|
||||
_ = cfg.Fprint(buf, newfset, newlit)
|
||||
if fJSON {
|
||||
output := struct {
|
||||
Start int `json:"start"`
|
||||
End int `json:"end"`
|
||||
Replacement string `json:"replacement"`
|
||||
}{
|
||||
oldfset.Position(oldlit.Pos()).Offset,
|
||||
oldfset.Position(oldlit.End()).Offset,
|
||||
buf.String(),
|
||||
}
|
||||
_ = json.NewEncoder(os.Stdout).Encode(output)
|
||||
} else {
|
||||
fmt.Println(buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func copyExpr(expr ast.Expr, line token.Pos) ast.Expr {
|
||||
switch expr := expr.(type) {
|
||||
case *ast.BasicLit:
|
||||
cp := *expr
|
||||
cp.ValuePos = 0
|
||||
return &cp
|
||||
case *ast.BinaryExpr:
|
||||
cp := *expr
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
cp.OpPos = 0
|
||||
cp.Y = copyExpr(cp.Y, line)
|
||||
return &cp
|
||||
case *ast.CallExpr:
|
||||
cp := *expr
|
||||
cp.Fun = copyExpr(cp.Fun, line)
|
||||
cp.Lparen = 0
|
||||
for i, v := range cp.Args {
|
||||
cp.Args[i] = copyExpr(v, line)
|
||||
}
|
||||
if cp.Ellipsis != 0 {
|
||||
cp.Ellipsis = line
|
||||
}
|
||||
cp.Rparen = 0
|
||||
return &cp
|
||||
case *ast.CompositeLit:
|
||||
cp := *expr
|
||||
cp.Type = copyExpr(cp.Type, line)
|
||||
cp.Lbrace = 0
|
||||
for i, v := range cp.Elts {
|
||||
cp.Elts[i] = copyExpr(v, line)
|
||||
}
|
||||
cp.Rbrace = 0
|
||||
return &cp
|
||||
case *ast.Ident:
|
||||
cp := *expr
|
||||
cp.NamePos = 0
|
||||
return &cp
|
||||
case *ast.IndexExpr:
|
||||
cp := *expr
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
cp.Lbrack = 0
|
||||
cp.Index = copyExpr(cp.Index, line)
|
||||
cp.Rbrack = 0
|
||||
return &cp
|
||||
case *ast.KeyValueExpr:
|
||||
cp := *expr
|
||||
cp.Key = copyExpr(cp.Key, line)
|
||||
cp.Colon = 0
|
||||
cp.Value = copyExpr(cp.Value, line)
|
||||
return &cp
|
||||
case *ast.ParenExpr:
|
||||
cp := *expr
|
||||
cp.Lparen = 0
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
cp.Rparen = 0
|
||||
return &cp
|
||||
case *ast.SelectorExpr:
|
||||
cp := *expr
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
cp.Sel = copyExpr(cp.Sel, line).(*ast.Ident)
|
||||
return &cp
|
||||
case *ast.SliceExpr:
|
||||
cp := *expr
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
cp.Lbrack = 0
|
||||
cp.Low = copyExpr(cp.Low, line)
|
||||
cp.High = copyExpr(cp.High, line)
|
||||
cp.Max = copyExpr(cp.Max, line)
|
||||
cp.Rbrack = 0
|
||||
return &cp
|
||||
case *ast.StarExpr:
|
||||
cp := *expr
|
||||
cp.Star = 0
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
return &cp
|
||||
case *ast.TypeAssertExpr:
|
||||
cp := *expr
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
cp.Lparen = 0
|
||||
cp.Type = copyExpr(cp.Type, line)
|
||||
cp.Rparen = 0
|
||||
return &cp
|
||||
case *ast.UnaryExpr:
|
||||
cp := *expr
|
||||
cp.OpPos = 0
|
||||
cp.X = copyExpr(cp.X, line)
|
||||
return &cp
|
||||
case *ast.MapType:
|
||||
cp := *expr
|
||||
cp.Map = 0
|
||||
cp.Key = copyExpr(cp.Key, line)
|
||||
cp.Value = copyExpr(cp.Value, line)
|
||||
return &cp
|
||||
case *ast.ArrayType:
|
||||
cp := *expr
|
||||
cp.Lbrack = 0
|
||||
cp.Len = copyExpr(cp.Len, line)
|
||||
cp.Elt = copyExpr(cp.Elt, line)
|
||||
return &cp
|
||||
case *ast.Ellipsis:
|
||||
cp := *expr
|
||||
cp.Elt = copyExpr(cp.Elt, line)
|
||||
cp.Ellipsis = line
|
||||
return &cp
|
||||
case *ast.InterfaceType:
|
||||
cp := *expr
|
||||
cp.Interface = 0
|
||||
return &cp
|
||||
case *ast.StructType:
|
||||
cp := *expr
|
||||
cp.Struct = 0
|
||||
return &cp
|
||||
case *ast.FuncLit:
|
||||
return expr
|
||||
case *ast.ChanType:
|
||||
cp := *expr
|
||||
cp.Arrow = 0
|
||||
cp.Begin = 0
|
||||
cp.Value = copyExpr(cp.Value, line)
|
||||
return &cp
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
panic(fmt.Sprintf("shouldn't happen: unknown ast.Expr of type %T", expr))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseOctothorpDecimal(s string) int {
|
||||
if s != "" && s[0] == '#' {
|
||||
if s, err := strconv.ParseInt(s[1:], 10, 32); err == nil {
|
||||
return int(s)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func parsePos(pos string) (filename string, startOffset, endOffset int, err error) {
|
||||
if pos == "" {
|
||||
err = fmt.Errorf("no source position specified")
|
||||
return
|
||||
}
|
||||
|
||||
colon := strings.LastIndex(pos, ":")
|
||||
if colon < 0 {
|
||||
err = fmt.Errorf("bad position syntax %q", pos)
|
||||
return
|
||||
}
|
||||
filename, offset := pos[:colon], pos[colon+1:]
|
||||
startOffset = -1
|
||||
endOffset = -1
|
||||
if hyphen := strings.Index(offset, ","); hyphen < 0 {
|
||||
// e.g. "foo.go:#123"
|
||||
startOffset = parseOctothorpDecimal(offset)
|
||||
endOffset = startOffset
|
||||
} else {
|
||||
// e.g. "foo.go:#123,#456"
|
||||
startOffset = parseOctothorpDecimal(offset[:hyphen])
|
||||
endOffset = parseOctothorpDecimal(offset[hyphen+1:])
|
||||
}
|
||||
if startOffset < 0 || endOffset < 0 {
|
||||
err = fmt.Errorf("invalid offset %q in query position", offset)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fileOffsetToPos(file *token.File, startOffset, endOffset int) (start, end token.Pos, err error) {
|
||||
// Range check [start..end], inclusive of both end-points.
|
||||
|
||||
if 0 <= startOffset && startOffset <= file.Size() {
|
||||
start = file.Pos(int(startOffset))
|
||||
} else {
|
||||
err = fmt.Errorf("start position is beyond end of file")
|
||||
return
|
||||
}
|
||||
|
||||
if 0 <= endOffset && endOffset <= file.Size() {
|
||||
end = file.Pos(int(endOffset))
|
||||
} else {
|
||||
err = fmt.Errorf("end position is beyond end of file")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# staticcheck
|
||||
|
||||
_staticcheck_ offers extensive analysis of Go code, covering a myriad
|
||||
of categories. It will detect bugs, suggest code simplifications,
|
||||
point out dead code, and more.
|
||||
|
||||
## Installation
|
||||
|
||||
See [the main README](https://github.com/dominikh/go-tools#installation) for installation instructions.
|
||||
|
||||
## Documentation
|
||||
|
||||
Detailed documentation can be found on
|
||||
[staticcheck.io](https://staticcheck.io/docs/).
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// staticcheck analyses Go code and makes it better.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"honnef.co/go/tools/lintcmd"
|
||||
"honnef.co/go/tools/lintcmd/version"
|
||||
"honnef.co/go/tools/quickfix"
|
||||
"honnef.co/go/tools/simple"
|
||||
"honnef.co/go/tools/staticcheck"
|
||||
"honnef.co/go/tools/stylecheck"
|
||||
"honnef.co/go/tools/unused"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := lintcmd.NewCommand("staticcheck")
|
||||
cmd.SetVersion(version.Version, version.MachineVersion)
|
||||
|
||||
fs := cmd.FlagSet()
|
||||
debug := fs.String("debug.unused-graph", "", "Write unused's object graph to `file`")
|
||||
qf := fs.Bool("debug.run-quickfix-analyzers", false, "Run quickfix analyzers")
|
||||
|
||||
cmd.ParseFlags(os.Args[1:])
|
||||
|
||||
cmd.AddAnalyzers(simple.Analyzers...)
|
||||
cmd.AddAnalyzers(staticcheck.Analyzers...)
|
||||
cmd.AddAnalyzers(stylecheck.Analyzers...)
|
||||
cmd.AddAnalyzers(unused.Analyzer)
|
||||
|
||||
if *qf {
|
||||
cmd.AddAnalyzers(quickfix.Analyzers...)
|
||||
}
|
||||
|
||||
if *debug != "" {
|
||||
f, err := os.OpenFile(*debug, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
unused.Debug = f
|
||||
}
|
||||
|
||||
cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// structlayout-optimize reorders struct fields to minimize the amount
|
||||
// of padding.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/lintcmd/version"
|
||||
st "honnef.co/go/tools/structlayout"
|
||||
)
|
||||
|
||||
var (
|
||||
fJSON bool
|
||||
fRecurse bool
|
||||
fVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&fJSON, "json", false, "Format data as JSON")
|
||||
flag.BoolVar(&fRecurse, "r", false, "Break up structs and reorder their fields freely")
|
||||
flag.BoolVar(&fVersion, "version", false, "Print version and exit")
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
|
||||
if fVersion {
|
||||
version.Print(version.Version, version.MachineVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var in []st.Field
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(in) == 0 {
|
||||
return
|
||||
}
|
||||
if !fRecurse {
|
||||
in = combine(in)
|
||||
}
|
||||
var fields []st.Field
|
||||
for _, field := range in {
|
||||
if field.IsPadding {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
optimize(fields)
|
||||
fields = pad(fields)
|
||||
|
||||
if fJSON {
|
||||
json.NewEncoder(os.Stdout).Encode(fields)
|
||||
} else {
|
||||
for _, field := range fields {
|
||||
fmt.Println(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func combine(fields []st.Field) []st.Field {
|
||||
new := st.Field{}
|
||||
cur := ""
|
||||
var out []st.Field
|
||||
wasPad := true
|
||||
for _, field := range fields {
|
||||
var prefix string
|
||||
if field.IsPadding {
|
||||
wasPad = true
|
||||
continue
|
||||
}
|
||||
p := strings.Split(field.Name, ".")
|
||||
prefix = strings.Join(p[:2], ".")
|
||||
if field.Align > new.Align {
|
||||
new.Align = field.Align
|
||||
}
|
||||
if !wasPad {
|
||||
new.End = field.Start
|
||||
new.Size = new.End - new.Start
|
||||
}
|
||||
if prefix != cur {
|
||||
if cur != "" {
|
||||
out = append(out, new)
|
||||
}
|
||||
cur = prefix
|
||||
new = field
|
||||
new.Name = prefix
|
||||
} else {
|
||||
new.Type = "struct"
|
||||
}
|
||||
wasPad = false
|
||||
}
|
||||
new.Size = new.End - new.Start
|
||||
out = append(out, new)
|
||||
return out
|
||||
}
|
||||
|
||||
func optimize(fields []st.Field) {
|
||||
sort.Sort(&byAlignAndSize{fields})
|
||||
}
|
||||
|
||||
func pad(fields []st.Field) []st.Field {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []st.Field
|
||||
pos := int64(0)
|
||||
offsets := offsetsof(fields)
|
||||
alignment := int64(1)
|
||||
for i, field := range fields {
|
||||
if field.Align > alignment {
|
||||
alignment = field.Align
|
||||
}
|
||||
if offsets[i] > pos {
|
||||
padding := offsets[i] - pos
|
||||
out = append(out, st.Field{
|
||||
IsPadding: true,
|
||||
Start: pos,
|
||||
End: pos + padding,
|
||||
Size: padding,
|
||||
})
|
||||
pos += padding
|
||||
}
|
||||
field.Start = pos
|
||||
field.End = pos + field.Size
|
||||
out = append(out, field)
|
||||
pos += field.Size
|
||||
}
|
||||
sz := size(out)
|
||||
pad := align(sz, alignment) - sz
|
||||
if pad > 0 {
|
||||
field := out[len(out)-1]
|
||||
out = append(out, st.Field{
|
||||
IsPadding: true,
|
||||
Start: field.End,
|
||||
End: field.End + pad,
|
||||
Size: pad,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func size(fields []st.Field) int64 {
|
||||
n := int64(0)
|
||||
for _, field := range fields {
|
||||
n += field.Size
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type byAlignAndSize struct {
|
||||
fields []st.Field
|
||||
}
|
||||
|
||||
func (s *byAlignAndSize) Len() int { return len(s.fields) }
|
||||
func (s *byAlignAndSize) Swap(i, j int) {
|
||||
s.fields[i], s.fields[j] = s.fields[j], s.fields[i]
|
||||
}
|
||||
|
||||
func (s *byAlignAndSize) Less(i, j int) bool {
|
||||
// Place zero sized objects before non-zero sized objects.
|
||||
if s.fields[i].Size == 0 && s.fields[j].Size != 0 {
|
||||
return true
|
||||
}
|
||||
if s.fields[j].Size == 0 && s.fields[i].Size != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Next, place more tightly aligned objects before less tightly aligned objects.
|
||||
if s.fields[i].Align != s.fields[j].Align {
|
||||
return s.fields[i].Align > s.fields[j].Align
|
||||
}
|
||||
|
||||
// Lastly, order by size.
|
||||
if s.fields[i].Size != s.fields[j].Size {
|
||||
return s.fields[i].Size > s.fields[j].Size
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func offsetsof(fields []st.Field) []int64 {
|
||||
offsets := make([]int64, len(fields))
|
||||
var o int64
|
||||
for i, f := range fields {
|
||||
a := f.Align
|
||||
o = align(o, a)
|
||||
offsets[i] = o
|
||||
o += f.Size
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// align returns the smallest y >= x such that y % a == 0.
|
||||
func align(x, a int64) int64 {
|
||||
y := x + a - 1
|
||||
return y - y%a
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// structlayout-pretty formats the output of structlayout with ASCII
|
||||
// art.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/lintcmd/version"
|
||||
st "honnef.co/go/tools/structlayout"
|
||||
)
|
||||
|
||||
var (
|
||||
fVerbose bool
|
||||
fVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&fVerbose, "v", false, "Do not compact consecutive bytes of fields")
|
||||
flag.BoolVar(&fVersion, "version", false, "Print version and exit")
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
|
||||
if fVersion {
|
||||
version.Print(version.Version, version.MachineVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var fields []st.Field
|
||||
if err := json.NewDecoder(os.Stdin).Decode(&fields); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
max := fields[len(fields)-1].End
|
||||
maxLength := len(fmt.Sprintf("%d", max))
|
||||
padding := strings.Repeat(" ", maxLength+2)
|
||||
format := fmt.Sprintf(" %%%dd ", maxLength)
|
||||
pos := int64(0)
|
||||
fmt.Println(padding + "+--------+")
|
||||
for _, field := range fields {
|
||||
name := field.Name + " " + field.Type
|
||||
if field.IsPadding {
|
||||
name = "padding"
|
||||
}
|
||||
fmt.Printf(format+"| | <- %s (size %d, align %d)\n", pos, name, field.Size, field.Align)
|
||||
fmt.Println(padding + "+--------+")
|
||||
|
||||
if fVerbose {
|
||||
for i := int64(0); i < field.Size-1; i++ {
|
||||
fmt.Printf(format+"| |\n", pos+i+1)
|
||||
fmt.Println(padding + "+--------+")
|
||||
}
|
||||
} else {
|
||||
if field.Size > 2 {
|
||||
fmt.Println(padding + "-........-")
|
||||
fmt.Println(padding + "+--------+")
|
||||
fmt.Printf(format+"| |\n", pos+field.Size-1)
|
||||
fmt.Println(padding + "+--------+")
|
||||
}
|
||||
}
|
||||
pos += field.Size
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
# structlayout
|
||||
|
||||
The _structlayout_ utility prints the layout of a struct – that is the
|
||||
byte offset and size of each field, respecting alignment/padding.
|
||||
|
||||
The information is printed in human-readable form by default, but can
|
||||
be emitted as JSON with the `-json` flag. This makes it easy to
|
||||
consume this information in other tools.
|
||||
|
||||
A utility called _structlayout-pretty_ takes this JSON and prints an
|
||||
ASCII graphic representing the memory layout.
|
||||
|
||||
_structlayout-optimize_ is another tool. Inspired by
|
||||
[maligned](https://github.com/mdempsky/maligned), it reads
|
||||
_structlayout_ JSON on stdin and reorders fields to minimize the
|
||||
amount of padding. The tool can itself emit JSON and feed into e.g.
|
||||
_structlayout-pretty_.
|
||||
|
||||
_structlayout-svg_ is a third-party tool that, similarly to
|
||||
_structlayout-pretty_, visualises struct layouts. It does so by
|
||||
generating a fancy-looking SVG graphic. You can install it via
|
||||
|
||||
```
|
||||
go get github.com/ajstarks/svgo/structlayout-svg
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
See [the main README](https://github.com/dominikh/go-tools#installation) for installation instructions.
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
$ structlayout bufio Reader
|
||||
Reader.buf []byte: 0-24 (24 bytes)
|
||||
Reader.rd io.Reader: 24-40 (16 bytes)
|
||||
Reader.r int: 40-48 (8 bytes)
|
||||
Reader.w int: 48-56 (8 bytes)
|
||||
Reader.err error: 56-72 (16 bytes)
|
||||
Reader.lastByte int: 72-80 (8 bytes)
|
||||
Reader.lastRuneSize int: 80-88 (8 bytes)
|
||||
```
|
||||
|
||||
```
|
||||
$ structlayout -json bufio Reader | jq .
|
||||
[
|
||||
{
|
||||
"name": "Reader.buf",
|
||||
"type": "[]byte",
|
||||
"start": 0,
|
||||
"end": 24,
|
||||
"size": 24,
|
||||
"is_padding": false
|
||||
},
|
||||
{
|
||||
"name": "Reader.rd",
|
||||
"type": "io.Reader",
|
||||
"start": 24,
|
||||
"end": 40,
|
||||
"size": 16,
|
||||
"is_padding": false
|
||||
},
|
||||
{
|
||||
"name": "Reader.r",
|
||||
"type": "int",
|
||||
"start": 40,
|
||||
"end": 48,
|
||||
"size": 8,
|
||||
"is_padding": false
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
```
|
||||
$ structlayout -json bufio Reader | structlayout-pretty
|
||||
+--------+
|
||||
0 | | <- Reader.buf []byte
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
23 | |
|
||||
+--------+
|
||||
24 | | <- Reader.rd io.Reader
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
39 | |
|
||||
+--------+
|
||||
40 | | <- Reader.r int
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
47 | |
|
||||
+--------+
|
||||
48 | | <- Reader.w int
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
55 | |
|
||||
+--------+
|
||||
56 | | <- Reader.err error
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
71 | |
|
||||
+--------+
|
||||
72 | | <- Reader.lastByte int
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
79 | |
|
||||
+--------+
|
||||
80 | | <- Reader.lastRuneSize int
|
||||
+--------+
|
||||
-........-
|
||||
+--------+
|
||||
87 | |
|
||||
+--------+
|
||||
```
|
||||
|
||||
```
|
||||
$ structlayout -json bytes Buffer | structlayout-svg -t "bytes.Buffer" > /tmp/struct.svg
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,151 @@
|
||||
// structlayout displays the layout (field sizes and padding) of structs.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/types"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"honnef.co/go/tools/go/gcsizes"
|
||||
"honnef.co/go/tools/lintcmd/version"
|
||||
st "honnef.co/go/tools/structlayout"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
var (
|
||||
fJSON bool
|
||||
fVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&fJSON, "json", false, "Format data as JSON")
|
||||
flag.BoolVar(&fVersion, "version", false, "Print version and exit")
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
|
||||
if fVersion {
|
||||
version.Print(version.Version, version.MachineVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if len(flag.Args()) != 2 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedImports | packages.NeedExportFile | packages.NeedTypes | packages.NeedSyntax,
|
||||
Tests: true,
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, flag.Args()[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
typName := flag.Args()[1]
|
||||
|
||||
var typ types.Type
|
||||
obj := pkg.Types.Scope().Lookup(typName)
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
typ = obj.Type()
|
||||
|
||||
st, ok := typ.Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
log.Fatal("identifier is not a struct type")
|
||||
}
|
||||
|
||||
fields := sizes(st, typ.(*types.Named).Obj().Name(), 0, nil)
|
||||
if fJSON {
|
||||
emitJSON(fields)
|
||||
} else {
|
||||
emitText(fields)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Fatal("couldn't find type")
|
||||
}
|
||||
|
||||
func emitJSON(fields []st.Field) {
|
||||
if fields == nil {
|
||||
fields = []st.Field{}
|
||||
}
|
||||
json.NewEncoder(os.Stdout).Encode(fields)
|
||||
}
|
||||
|
||||
func emitText(fields []st.Field) {
|
||||
for _, field := range fields {
|
||||
fmt.Println(field)
|
||||
}
|
||||
}
|
||||
func sizes(typ *types.Struct, prefix string, base int64, out []st.Field) []st.Field {
|
||||
s := gcsizes.ForArch(build.Default.GOARCH)
|
||||
n := typ.NumFields()
|
||||
var fields []*types.Var
|
||||
for i := 0; i < n; i++ {
|
||||
fields = append(fields, typ.Field(i))
|
||||
}
|
||||
offsets := s.Offsetsof(fields)
|
||||
for i := range offsets {
|
||||
offsets[i] += base
|
||||
}
|
||||
|
||||
pos := base
|
||||
for i, field := range fields {
|
||||
if offsets[i] > pos {
|
||||
padding := offsets[i] - pos
|
||||
out = append(out, st.Field{
|
||||
IsPadding: true,
|
||||
Start: pos,
|
||||
End: pos + padding,
|
||||
Size: padding,
|
||||
})
|
||||
pos += padding
|
||||
}
|
||||
size := s.Sizeof(field.Type())
|
||||
if typ2, ok := field.Type().Underlying().(*types.Struct); ok && typ2.NumFields() != 0 {
|
||||
out = sizes(typ2, prefix+"."+field.Name(), pos, out)
|
||||
} else {
|
||||
out = append(out, st.Field{
|
||||
Name: prefix + "." + field.Name(),
|
||||
Type: field.Type().String(),
|
||||
Start: offsets[i],
|
||||
End: offsets[i] + size,
|
||||
Size: size,
|
||||
Align: s.Alignof(field.Type()),
|
||||
})
|
||||
}
|
||||
pos += size
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return out
|
||||
}
|
||||
field := &out[len(out)-1]
|
||||
if field.Size == 0 {
|
||||
field.Size = 1
|
||||
field.End++
|
||||
}
|
||||
pad := s.Sizeof(typ) - field.End
|
||||
if pad > 0 {
|
||||
out = append(out, st.Field{
|
||||
IsPadding: true,
|
||||
Start: field.End,
|
||||
End: field.End + pad,
|
||||
Size: pad,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"honnef.co/go/tools/go/loader"
|
||||
"honnef.co/go/tools/lintcmd/cache"
|
||||
"honnef.co/go/tools/unused"
|
||||
)
|
||||
|
||||
// OPT(dh): we don't need full graph merging if we're not flagging exported objects. In that case, we can reuse the old
|
||||
// list-based merging approach.
|
||||
|
||||
// OPT(dh): we can either merge graphs as we process packages, or we can merge them all in one go afterwards (then
|
||||
// reloading them from cache). The first approach will likely lead to higher peak memory usage, but the latter may take
|
||||
// more wall time to finish if we had spare CPU resources while processing packages.
|
||||
|
||||
func main() {
|
||||
opts := unused.DefaultOptions
|
||||
flag.BoolVar(&opts.FieldWritesAreUses, "field-writes-are-uses", opts.FieldWritesAreUses, "")
|
||||
flag.BoolVar(&opts.PostStatementsAreReads, "post-statements-are-reads", opts.PostStatementsAreReads, "")
|
||||
flag.BoolVar(&opts.ExportedIsUsed, "exported-is-used", opts.ExportedIsUsed, "")
|
||||
flag.BoolVar(&opts.ExportedFieldsAreUsed, "exported-fields-are-used", opts.ExportedFieldsAreUsed, "")
|
||||
flag.BoolVar(&opts.ParametersAreUsed, "parameters-are-used", opts.ParametersAreUsed, "")
|
||||
flag.BoolVar(&opts.LocalVariablesAreUsed, "local-variables-are-used", opts.LocalVariablesAreUsed, "")
|
||||
flag.BoolVar(&opts.GeneratedIsUsed, "generated-is-used", opts.GeneratedIsUsed, "")
|
||||
flag.Parse()
|
||||
|
||||
// pprof.StartCPUProfile(os.Stdout)
|
||||
// defer pprof.StopCPUProfile()
|
||||
|
||||
// XXX set cache key for this tool
|
||||
|
||||
c, err := cache.Default()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cfg := &packages.Config{
|
||||
Tests: true,
|
||||
}
|
||||
specs, err := loader.Graph(c, cfg, flag.Args()...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var sg unused.SerializedGraph
|
||||
|
||||
ourPkgs := map[string]struct{}{}
|
||||
|
||||
for _, spec := range specs {
|
||||
if len(spec.Errors) != 0 {
|
||||
// XXX priunt errors
|
||||
continue
|
||||
}
|
||||
lpkg, _, err := loader.Load(spec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(lpkg.Errors) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// XXX get directives and generated
|
||||
g := unused.Graph(lpkg.Fset, lpkg.Syntax, lpkg.Types, lpkg.TypesInfo, nil, nil, opts)
|
||||
sg.Merge(g)
|
||||
ourPkgs[spec.PkgPath] = struct{}{}
|
||||
}
|
||||
|
||||
res := sg.Results()
|
||||
for _, obj := range res.Unused {
|
||||
// XXX format paths like staticcheck does
|
||||
if _, ok := ourPkgs[obj.Path.PkgPath]; !ok {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s: %s %s is unused\n", obj.DisplayPosition, obj.Kind, obj.Name)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, sg.Dot())
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
// Dir looks at a list of absolute file names, which should make up a
|
||||
// single package, and returns the path of the directory that may
|
||||
// contain a staticcheck.conf file. It returns the empty string if no
|
||||
// such directory could be determined, for example because all files
|
||||
// were located in Go's build cache.
|
||||
func Dir(files []string) string {
|
||||
if len(files) == 0 {
|
||||
return ""
|
||||
}
|
||||
cache, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
cache = ""
|
||||
}
|
||||
var path string
|
||||
for _, p := range files {
|
||||
// FIXME(dh): using strings.HasPrefix isn't technically
|
||||
// correct, but it should be good enough for now.
|
||||
if cache != "" && strings.HasPrefix(p, cache) {
|
||||
// File in the build cache of the standard Go build system
|
||||
continue
|
||||
}
|
||||
path = p
|
||||
break
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
// The package only consists of generated files.
|
||||
return ""
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
return dir
|
||||
}
|
||||
|
||||
func dirAST(files []*ast.File, fset *token.FileSet) string {
|
||||
names := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
names[i] = fset.PositionFor(f.Pos(), true).Filename
|
||||
}
|
||||
return Dir(names)
|
||||
}
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "config",
|
||||
Doc: "loads configuration for the current package tree",
|
||||
Run: func(pass *analysis.Pass) (interface{}, error) {
|
||||
dir := dirAST(pass.Files, pass.Fset)
|
||||
if dir == "" {
|
||||
cfg := DefaultConfig
|
||||
return &cfg, nil
|
||||
}
|
||||
cfg, err := Load(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading staticcheck.conf: %s", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
},
|
||||
RunDespiteErrors: true,
|
||||
ResultType: reflect.TypeOf((*Config)(nil)),
|
||||
}
|
||||
|
||||
func For(pass *analysis.Pass) *Config {
|
||||
return pass.ResultOf[Analyzer].(*Config)
|
||||
}
|
||||
|
||||
func mergeLists(a, b []string) []string {
|
||||
out := make([]string, 0, len(a)+len(b))
|
||||
for _, el := range b {
|
||||
if el == "inherit" {
|
||||
out = append(out, a...)
|
||||
} else {
|
||||
out = append(out, el)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeList(list []string) []string {
|
||||
if len(list) > 1 {
|
||||
nlist := make([]string, 0, len(list))
|
||||
nlist = append(nlist, list[0])
|
||||
for i, el := range list[1:] {
|
||||
if el != list[i] {
|
||||
nlist = append(nlist, el)
|
||||
}
|
||||
}
|
||||
list = nlist
|
||||
}
|
||||
|
||||
for _, el := range list {
|
||||
if el == "inherit" {
|
||||
// This should never happen, because the default config
|
||||
// should not use "inherit"
|
||||
panic(`unresolved "inherit"`)
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (cfg Config) Merge(ocfg Config) Config {
|
||||
if ocfg.Checks != nil {
|
||||
cfg.Checks = mergeLists(cfg.Checks, ocfg.Checks)
|
||||
}
|
||||
if ocfg.Initialisms != nil {
|
||||
cfg.Initialisms = mergeLists(cfg.Initialisms, ocfg.Initialisms)
|
||||
}
|
||||
if ocfg.DotImportWhitelist != nil {
|
||||
cfg.DotImportWhitelist = mergeLists(cfg.DotImportWhitelist, ocfg.DotImportWhitelist)
|
||||
}
|
||||
if ocfg.HTTPStatusCodeWhitelist != nil {
|
||||
cfg.HTTPStatusCodeWhitelist = mergeLists(cfg.HTTPStatusCodeWhitelist, ocfg.HTTPStatusCodeWhitelist)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// TODO(dh): this implementation makes it impossible for external
|
||||
// clients to add their own checkers with configuration. At the
|
||||
// moment, we don't really care about that; we don't encourage
|
||||
// that people use this package. In the future, we may. The
|
||||
// obvious solution would be using map[string]interface{}, but
|
||||
// that's obviously subpar.
|
||||
|
||||
Checks []string `toml:"checks"`
|
||||
Initialisms []string `toml:"initialisms"`
|
||||
DotImportWhitelist []string `toml:"dot_import_whitelist"`
|
||||
HTTPStatusCodeWhitelist []string `toml:"http_status_code_whitelist"`
|
||||
}
|
||||
|
||||
func (c Config) String() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
fmt.Fprintf(buf, "Checks: %#v\n", c.Checks)
|
||||
fmt.Fprintf(buf, "Initialisms: %#v\n", c.Initialisms)
|
||||
fmt.Fprintf(buf, "DotImportWhitelist: %#v\n", c.DotImportWhitelist)
|
||||
fmt.Fprintf(buf, "HTTPStatusCodeWhitelist: %#v", c.HTTPStatusCodeWhitelist)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// DefaultConfig is the default configuration.
|
||||
// Its initial value describes the majority of the default configuration,
|
||||
// but the Checks field can be updated at runtime based on the analyzers being used, to disable non-default checks.
|
||||
// For cmd/staticcheck, this is handled by (*lintcmd.Command).Run.
|
||||
//
|
||||
// Note that DefaultConfig shouldn't be modified while analyzers are executing.
|
||||
var DefaultConfig = Config{
|
||||
Checks: []string{"all"},
|
||||
Initialisms: []string{
|
||||
"ACL", "API", "ASCII", "CPU", "CSS", "DNS",
|
||||
"EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID",
|
||||
"IP", "JSON", "QPS", "RAM", "RPC", "SLA",
|
||||
"SMTP", "SQL", "SSH", "TCP", "TLS", "TTL",
|
||||
"UDP", "UI", "GID", "UID", "UUID", "URI",
|
||||
"URL", "UTF8", "VM", "XML", "XMPP", "XSRF",
|
||||
"XSS", "SIP", "RTP", "AMQP", "DB", "TS",
|
||||
},
|
||||
DotImportWhitelist: []string{
|
||||
"github.com/mmcloughlin/avo/build",
|
||||
"github.com/mmcloughlin/avo/operand",
|
||||
"github.com/mmcloughlin/avo/reg",
|
||||
},
|
||||
HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"},
|
||||
}
|
||||
|
||||
const ConfigName = "staticcheck.conf"
|
||||
|
||||
type ParseError struct {
|
||||
Filename string
|
||||
toml.ParseError
|
||||
}
|
||||
|
||||
func parseConfigs(dir string) ([]Config, error) {
|
||||
var out []Config
|
||||
|
||||
// TODO(dh): consider stopping at the GOPATH/module boundary
|
||||
for dir != "" {
|
||||
f, err := os.Open(filepath.Join(dir, ConfigName))
|
||||
if os.IsNotExist(err) {
|
||||
ndir := filepath.Dir(dir)
|
||||
if ndir == dir {
|
||||
break
|
||||
}
|
||||
dir = ndir
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg Config
|
||||
_, err = toml.NewDecoder(f).Decode(&cfg)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
if err, ok := err.(toml.ParseError); ok {
|
||||
return nil, ParseError{
|
||||
Filename: filepath.Join(dir, ConfigName),
|
||||
ParseError: err,
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, cfg)
|
||||
ndir := filepath.Dir(dir)
|
||||
if ndir == dir {
|
||||
break
|
||||
}
|
||||
dir = ndir
|
||||
}
|
||||
out = append(out, DefaultConfig)
|
||||
if len(out) < 2 {
|
||||
return out, nil
|
||||
}
|
||||
for i := 0; i < len(out)/2; i++ {
|
||||
out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func mergeConfigs(confs []Config) Config {
|
||||
if len(confs) == 0 {
|
||||
// This shouldn't happen because we always have at least a
|
||||
// default config.
|
||||
panic("trying to merge zero configs")
|
||||
}
|
||||
if len(confs) == 1 {
|
||||
return confs[0]
|
||||
}
|
||||
conf := confs[0]
|
||||
for _, oconf := range confs[1:] {
|
||||
conf = conf.Merge(oconf)
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
func Load(dir string) (Config, error) {
|
||||
confs, err := parseConfigs(dir)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
conf := mergeConfigs(confs)
|
||||
|
||||
conf.Checks = normalizeList(conf.Checks)
|
||||
conf.Initialisms = normalizeList(conf.Initialisms)
|
||||
conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist)
|
||||
conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist)
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"]
|
||||
initialisms = ["ACL", "API", "ASCII", "CPU", "CSS", "DNS",
|
||||
"EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID",
|
||||
"IP", "JSON", "QPS", "RAM", "RPC", "SLA",
|
||||
"SMTP", "SQL", "SSH", "TCP", "TLS", "TTL",
|
||||
"UDP", "UI", "GID", "UID", "UUID", "URI",
|
||||
"URL", "UTF8", "VM", "XML", "XMPP", "XSRF",
|
||||
"XSS", "SIP", "RTP", "AMQP", "DB", "TS"]
|
||||
dot_import_whitelist = [
|
||||
"github.com/mmcloughlin/avo/build",
|
||||
"github.com/mmcloughlin/avo/operand",
|
||||
"github.com/mmcloughlin/avo/reg",
|
||||
]
|
||||
http_status_code_whitelist = ["200", "400", "404", "500"]
|
||||
@@ -0,0 +1,47 @@
|
||||
// Package debug contains helpers for debugging static analyses.
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
// TypeCheck parses and type-checks a single-file Go package from a string.
|
||||
// The package must not have any imports.
|
||||
func TypeCheck(src string) (*ast.File, *types.Package, *types.Info, error) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments|parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
pkg := types.NewPackage("foo", f.Name.Name)
|
||||
info := &types.Info{
|
||||
Types: map[ast.Expr]types.TypeAndValue{},
|
||||
Defs: map[*ast.Ident]types.Object{},
|
||||
Uses: map[*ast.Ident]types.Object{},
|
||||
Implicits: map[ast.Node]types.Object{},
|
||||
Selections: map[*ast.SelectorExpr]*types.Selection{},
|
||||
Scopes: map[ast.Node]*types.Scope{},
|
||||
InitOrder: []*types.Initializer{},
|
||||
Instances: map[*ast.Ident]types.Instance{},
|
||||
}
|
||||
tcfg := &types.Config{
|
||||
Importer: importer.Default(),
|
||||
}
|
||||
if err := types.NewChecker(tcfg, fset, pkg, info).Files([]*ast.File{f}); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return f, pkg, info, nil
|
||||
}
|
||||
|
||||
func FormatNode(node ast.Node) string {
|
||||
var buf bytes.Buffer
|
||||
fset := token.NewFileSet()
|
||||
format.Node(&buf, fset, node)
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
|
||||
build() {
|
||||
ROOT="$GOPATH/src/honnef.co/go/tools"
|
||||
|
||||
os="$1"
|
||||
arch="$2"
|
||||
|
||||
echo "Building GOOS=$os GOARCH=$arch..."
|
||||
exe="staticcheck"
|
||||
if [ $os = "windows" ]; then
|
||||
exe="${exe}.exe"
|
||||
fi
|
||||
target="staticcheck_${os}_${arch}"
|
||||
|
||||
arm=""
|
||||
case "$arch" in
|
||||
armv5l)
|
||||
arm=5
|
||||
arch=arm
|
||||
;;
|
||||
armv6l)
|
||||
arm=6
|
||||
arch=arm
|
||||
;;
|
||||
armv7l)
|
||||
arm=7
|
||||
arch=arm
|
||||
;;
|
||||
arm64)
|
||||
arch=arm64
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir "$d/staticcheck"
|
||||
cp "$ROOT/LICENSE" "$ROOT/LICENSE-THIRD-PARTY" "$d/staticcheck"
|
||||
CGO_ENABLED=0 GOOS=$os GOARCH=$arch GOARM=$arm GO111MODULE=on go build -trimpath -o "$d/staticcheck/$exe" honnef.co/go/tools/cmd/staticcheck
|
||||
(
|
||||
cd "$d"
|
||||
tar -czf "$target.tar.gz" staticcheck
|
||||
sha256sum "$target.tar.gz" > "$target.tar.gz.sha256"
|
||||
)
|
||||
rm -rf "$d/staticcheck"
|
||||
}
|
||||
|
||||
rev="$1"
|
||||
if [ -z "$rev" ]; then
|
||||
echo "Usage: $0 <version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
mkdir "$rev"
|
||||
d=$(realpath "$rev")
|
||||
|
||||
wrk=$(mktemp -d)
|
||||
trap "{ rm -rf \"$wrk\"; }" EXIT
|
||||
cd "$wrk"
|
||||
|
||||
go mod init foo
|
||||
GO111MODULE=on go get -d honnef.co/go/tools/cmd/staticcheck@"$rev"
|
||||
|
||||
|
||||
SYSTEMS=(windows linux freebsd)
|
||||
ARCHS=(amd64 386)
|
||||
for os in ${SYSTEMS[@]}; do
|
||||
for arch in ${ARCHS[@]}; do
|
||||
build "$os" "$arch"
|
||||
done
|
||||
done
|
||||
|
||||
build "darwin" "amd64"
|
||||
build "darwin" "arm64"
|
||||
|
||||
for arch in armv5l armv6l armv7l arm64; do
|
||||
build "linux" "$arch"
|
||||
done
|
||||
|
||||
(
|
||||
cd "$d"
|
||||
sha256sum -c --strict *.sha256
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
- how to customize staticcheck
|
||||
- tools serve humans
|
||||
- tools should assist workflows
|
||||
- don't let tools bully you
|
||||
|
||||
- exit status
|
||||
- which checks run
|
||||
- ignoring findings
|
||||
- output format
|
||||
- go version
|
||||
- tests
|
||||
@@ -0,0 +1,45 @@
|
||||
<h2>Running Staticcheck</h2>
|
||||
|
||||
<h3>Checking packages</h3>
|
||||
|
||||
<p>
|
||||
The <code>staticcheck</code> command works much like <code>go build</code> or <code>go vet</code> do.
|
||||
It supports all of the same package patterns.
|
||||
For example, <code>staticcheck .</code> will check the current package, and <code>staticcheck ./...</code> will check all packages.
|
||||
For more details on specifying packages to check, see <code>go help packages</code>
|
||||
</p>
|
||||
|
||||
<h3>Explaining checks</h3>
|
||||
|
||||
<p>
|
||||
You can use <code>staticcheck -explain <check></code> to get a helpful description of a check.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Every diagnostic that <code>staticcheck</code> reports is annotated with the identifier of the specific check that found the issue.
|
||||
For example, in
|
||||
</p>
|
||||
|
||||
<pre><code>foo.go:1248:4: unnecessary use of fmt.Sprintf (S1039)</code></pre>
|
||||
|
||||
<p>
|
||||
the check's identifier is <code>S1039</code>.
|
||||
Running <code>staticcheck -explain S1039</code> will output the following:
|
||||
</p>
|
||||
|
||||
<pre><code>Unnecessary use of fmt.Sprint
|
||||
|
||||
Calling fmt.Sprint with a single string argument is unnecessary and identical to using the string directly.
|
||||
|
||||
Available since
|
||||
2020.1
|
||||
|
||||
Online documentation
|
||||
https://staticcheck.io/docs/checks#S1039</code></pre>
|
||||
|
||||
<p>
|
||||
The output includes a one-line summary,
|
||||
one or more paragraphs of helpful text,
|
||||
the first version of Staticcheck that the check appeared in,
|
||||
and a link to online documentation, which contains the same information as the output of <code>staticcheck -explain</code>.
|
||||
</p>
|
||||
@@ -0,0 +1,12 @@
|
||||
module honnef.co/go/tools
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1
|
||||
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5
|
||||
)
|
||||
|
||||
require golang.org/x/mod v0.12.0 // indirect
|
||||
@@ -0,0 +1,11 @@
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5 h1:Vk4mysSz+GqQK2eqgWbo4zEO89wkeAjJiFIr9bpqa8k=
|
||||
golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
@@ -0,0 +1,20 @@
|
||||
package astutil
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
_ "unsafe"
|
||||
|
||||
"golang.org/x/tools/go/ast/astutil"
|
||||
)
|
||||
|
||||
type Cursor = astutil.Cursor
|
||||
type ApplyFunc = astutil.ApplyFunc
|
||||
|
||||
func Apply(root ast.Node, pre, post ApplyFunc) (result ast.Node) {
|
||||
return astutil.Apply(root, pre, post)
|
||||
}
|
||||
|
||||
func PathEnclosingInterval(root *ast.File, start, end token.Pos) (path []ast.Node, exact bool) {
|
||||
return astutil.PathEnclosingInterval(root, start, end)
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package astutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsIdent(expr ast.Expr, ident string) bool {
|
||||
id, ok := expr.(*ast.Ident)
|
||||
return ok && id.Name == ident
|
||||
}
|
||||
|
||||
// isBlank returns whether id is the blank identifier "_".
|
||||
// If id == nil, the answer is false.
|
||||
func IsBlank(id ast.Expr) bool {
|
||||
ident, _ := id.(*ast.Ident)
|
||||
return ident != nil && ident.Name == "_"
|
||||
}
|
||||
|
||||
// Deprecated: use code.IsIntegerLiteral instead.
|
||||
func IsIntLiteral(expr ast.Expr, literal string) bool {
|
||||
lit, ok := expr.(*ast.BasicLit)
|
||||
return ok && lit.Kind == token.INT && lit.Value == literal
|
||||
}
|
||||
|
||||
// Deprecated: use IsIntLiteral instead
|
||||
func IsZero(expr ast.Expr) bool {
|
||||
return IsIntLiteral(expr, "0")
|
||||
}
|
||||
|
||||
func Preamble(f *ast.File) string {
|
||||
cutoff := f.Package
|
||||
if f.Doc != nil {
|
||||
cutoff = f.Doc.Pos()
|
||||
}
|
||||
var out []string
|
||||
for _, cmt := range f.Comments {
|
||||
if cmt.Pos() >= cutoff {
|
||||
break
|
||||
}
|
||||
out = append(out, cmt.Text())
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
func GroupSpecs(fset *token.FileSet, specs []ast.Spec) [][]ast.Spec {
|
||||
if len(specs) == 0 {
|
||||
return nil
|
||||
}
|
||||
groups := make([][]ast.Spec, 1)
|
||||
groups[0] = append(groups[0], specs[0])
|
||||
|
||||
for _, spec := range specs[1:] {
|
||||
g := groups[len(groups)-1]
|
||||
if fset.PositionFor(spec.Pos(), false).Line-1 !=
|
||||
fset.PositionFor(g[len(g)-1].End(), false).Line {
|
||||
|
||||
groups = append(groups, nil)
|
||||
}
|
||||
|
||||
groups[len(groups)-1] = append(groups[len(groups)-1], spec)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// Unparen returns e with any enclosing parentheses stripped.
|
||||
func Unparen(e ast.Expr) ast.Expr {
|
||||
for {
|
||||
p, ok := e.(*ast.ParenExpr)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
e = p.X
|
||||
}
|
||||
}
|
||||
|
||||
// CopyExpr creates a deep copy of an expression.
|
||||
// It doesn't support copying FuncLits and returns ok == false when encountering one.
|
||||
func CopyExpr(node ast.Expr) (ast.Expr, bool) {
|
||||
switch node := node.(type) {
|
||||
case *ast.BasicLit:
|
||||
cp := *node
|
||||
return &cp, true
|
||||
case *ast.BinaryExpr:
|
||||
cp := *node
|
||||
var ok1, ok2 bool
|
||||
cp.X, ok1 = CopyExpr(cp.X)
|
||||
cp.Y, ok2 = CopyExpr(cp.Y)
|
||||
return &cp, ok1 && ok2
|
||||
case *ast.CallExpr:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.Fun, ok = CopyExpr(cp.Fun)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
cp.Args = make([]ast.Expr, len(node.Args))
|
||||
for i, v := range node.Args {
|
||||
cp.Args[i], ok = CopyExpr(v)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return &cp, true
|
||||
case *ast.CompositeLit:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.Type, ok = CopyExpr(cp.Type)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
cp.Elts = make([]ast.Expr, len(node.Elts))
|
||||
for i, v := range node.Elts {
|
||||
cp.Elts[i], ok = CopyExpr(v)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return &cp, true
|
||||
case *ast.Ident:
|
||||
cp := *node
|
||||
return &cp, true
|
||||
case *ast.IndexExpr:
|
||||
var ok1, ok2 bool
|
||||
cp := *node
|
||||
cp.X, ok1 = CopyExpr(cp.X)
|
||||
cp.Index, ok2 = CopyExpr(cp.Index)
|
||||
return &cp, ok1 && ok2
|
||||
case *ast.IndexListExpr:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.X, ok = CopyExpr(cp.X)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
for i, v := range node.Indices {
|
||||
cp.Indices[i], ok = CopyExpr(v)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return &cp, true
|
||||
case *ast.KeyValueExpr:
|
||||
var ok1, ok2 bool
|
||||
cp := *node
|
||||
cp.Key, ok1 = CopyExpr(cp.Key)
|
||||
cp.Value, ok2 = CopyExpr(cp.Value)
|
||||
return &cp, ok1 && ok2
|
||||
case *ast.ParenExpr:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.X, ok = CopyExpr(cp.X)
|
||||
return &cp, ok
|
||||
case *ast.SelectorExpr:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.X, ok = CopyExpr(cp.X)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
sel, ok := CopyExpr(cp.Sel)
|
||||
if !ok {
|
||||
// this is impossible
|
||||
return nil, false
|
||||
}
|
||||
cp.Sel = sel.(*ast.Ident)
|
||||
return &cp, true
|
||||
case *ast.SliceExpr:
|
||||
var ok1, ok2, ok3, ok4 bool
|
||||
cp := *node
|
||||
cp.X, ok1 = CopyExpr(cp.X)
|
||||
cp.Low, ok2 = CopyExpr(cp.Low)
|
||||
cp.High, ok3 = CopyExpr(cp.High)
|
||||
cp.Max, ok4 = CopyExpr(cp.Max)
|
||||
return &cp, ok1 && ok2 && ok3 && ok4
|
||||
case *ast.StarExpr:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.X, ok = CopyExpr(cp.X)
|
||||
return &cp, ok
|
||||
case *ast.TypeAssertExpr:
|
||||
var ok1, ok2 bool
|
||||
cp := *node
|
||||
cp.X, ok1 = CopyExpr(cp.X)
|
||||
cp.Type, ok2 = CopyExpr(cp.Type)
|
||||
return &cp, ok1 && ok2
|
||||
case *ast.UnaryExpr:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.X, ok = CopyExpr(cp.X)
|
||||
return &cp, ok
|
||||
case *ast.MapType:
|
||||
var ok1, ok2 bool
|
||||
cp := *node
|
||||
cp.Key, ok1 = CopyExpr(cp.Key)
|
||||
cp.Value, ok2 = CopyExpr(cp.Value)
|
||||
return &cp, ok1 && ok2
|
||||
case *ast.ArrayType:
|
||||
var ok1, ok2 bool
|
||||
cp := *node
|
||||
cp.Len, ok1 = CopyExpr(cp.Len)
|
||||
cp.Elt, ok2 = CopyExpr(cp.Elt)
|
||||
return &cp, ok1 && ok2
|
||||
case *ast.Ellipsis:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.Elt, ok = CopyExpr(cp.Elt)
|
||||
return &cp, ok
|
||||
case *ast.InterfaceType:
|
||||
cp := *node
|
||||
return &cp, true
|
||||
case *ast.StructType:
|
||||
cp := *node
|
||||
return &cp, true
|
||||
case *ast.FuncLit, *ast.FuncType:
|
||||
// TODO(dh): implement copying of function literals and types.
|
||||
return nil, false
|
||||
case *ast.ChanType:
|
||||
var ok bool
|
||||
cp := *node
|
||||
cp.Value, ok = CopyExpr(cp.Value)
|
||||
return &cp, ok
|
||||
case nil:
|
||||
return nil, true
|
||||
default:
|
||||
panic(fmt.Sprintf("unreachable: %T", node))
|
||||
}
|
||||
}
|
||||
|
||||
func Equal(a, b ast.Node) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if reflect.TypeOf(a) != reflect.TypeOf(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch a := a.(type) {
|
||||
case *ast.BasicLit:
|
||||
b := b.(*ast.BasicLit)
|
||||
return a.Kind == b.Kind && a.Value == b.Value
|
||||
case *ast.BinaryExpr:
|
||||
b := b.(*ast.BinaryExpr)
|
||||
return Equal(a.X, b.X) && a.Op == b.Op && Equal(a.Y, b.Y)
|
||||
case *ast.CallExpr:
|
||||
b := b.(*ast.CallExpr)
|
||||
if len(a.Args) != len(b.Args) {
|
||||
return false
|
||||
}
|
||||
for i, arg := range a.Args {
|
||||
if !Equal(arg, b.Args[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return Equal(a.Fun, b.Fun) &&
|
||||
(a.Ellipsis == token.NoPos && b.Ellipsis == token.NoPos || a.Ellipsis != token.NoPos && b.Ellipsis != token.NoPos)
|
||||
case *ast.CompositeLit:
|
||||
b := b.(*ast.CompositeLit)
|
||||
if len(a.Elts) != len(b.Elts) {
|
||||
return false
|
||||
}
|
||||
for i, elt := range b.Elts {
|
||||
if !Equal(elt, b.Elts[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return Equal(a.Type, b.Type) && a.Incomplete == b.Incomplete
|
||||
case *ast.Ident:
|
||||
b := b.(*ast.Ident)
|
||||
return a.Name == b.Name
|
||||
case *ast.IndexExpr:
|
||||
b := b.(*ast.IndexExpr)
|
||||
return Equal(a.X, b.X) && Equal(a.Index, b.Index)
|
||||
case *ast.IndexListExpr:
|
||||
b := b.(*ast.IndexListExpr)
|
||||
if len(a.Indices) != len(b.Indices) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a.Indices {
|
||||
if !Equal(v, b.Indices[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return Equal(a.X, b.X)
|
||||
case *ast.KeyValueExpr:
|
||||
b := b.(*ast.KeyValueExpr)
|
||||
return Equal(a.Key, b.Key) && Equal(a.Value, b.Value)
|
||||
case *ast.ParenExpr:
|
||||
b := b.(*ast.ParenExpr)
|
||||
return Equal(a.X, b.X)
|
||||
case *ast.SelectorExpr:
|
||||
b := b.(*ast.SelectorExpr)
|
||||
return Equal(a.X, b.X) && Equal(a.Sel, b.Sel)
|
||||
case *ast.SliceExpr:
|
||||
b := b.(*ast.SliceExpr)
|
||||
return Equal(a.X, b.X) && Equal(a.Low, b.Low) && Equal(a.High, b.High) && Equal(a.Max, b.Max) && a.Slice3 == b.Slice3
|
||||
case *ast.StarExpr:
|
||||
b := b.(*ast.StarExpr)
|
||||
return Equal(a.X, b.X)
|
||||
case *ast.TypeAssertExpr:
|
||||
b := b.(*ast.TypeAssertExpr)
|
||||
return Equal(a.X, b.X) && Equal(a.Type, b.Type)
|
||||
case *ast.UnaryExpr:
|
||||
b := b.(*ast.UnaryExpr)
|
||||
return a.Op == b.Op && Equal(a.X, b.X)
|
||||
case *ast.MapType:
|
||||
b := b.(*ast.MapType)
|
||||
return Equal(a.Key, b.Key) && Equal(a.Value, b.Value)
|
||||
case *ast.ArrayType:
|
||||
b := b.(*ast.ArrayType)
|
||||
return Equal(a.Len, b.Len) && Equal(a.Elt, b.Elt)
|
||||
case *ast.Ellipsis:
|
||||
b := b.(*ast.Ellipsis)
|
||||
return Equal(a.Elt, b.Elt)
|
||||
case *ast.InterfaceType:
|
||||
b := b.(*ast.InterfaceType)
|
||||
return a.Incomplete == b.Incomplete && Equal(a.Methods, b.Methods)
|
||||
case *ast.StructType:
|
||||
b := b.(*ast.StructType)
|
||||
return a.Incomplete == b.Incomplete && Equal(a.Fields, b.Fields)
|
||||
case *ast.FuncLit:
|
||||
// TODO(dh): support function literals
|
||||
return false
|
||||
case *ast.ChanType:
|
||||
b := b.(*ast.ChanType)
|
||||
return a.Dir == b.Dir && (a.Arrow == token.NoPos && b.Arrow == token.NoPos || a.Arrow != token.NoPos && b.Arrow != token.NoPos)
|
||||
case *ast.FieldList:
|
||||
b := b.(*ast.FieldList)
|
||||
if len(a.List) != len(b.List) {
|
||||
return false
|
||||
}
|
||||
for i, fieldA := range a.List {
|
||||
if !Equal(fieldA, b.List[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *ast.Field:
|
||||
b := b.(*ast.Field)
|
||||
if len(a.Names) != len(b.Names) {
|
||||
return false
|
||||
}
|
||||
for j, name := range a.Names {
|
||||
if !Equal(name, b.Names[j]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !Equal(a.Type, b.Type) || !Equal(a.Tag, b.Tag) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
panic(fmt.Sprintf("unreachable: %T", a))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
This package extracts buildid.go and note.go from cmd/internal/buildid/.
|
||||
|
||||
We have modified it to remove support for AIX big archive files, to cut down on our dependencies.
|
||||
|
||||
The last upstream commit we've looked at was: 639acdc833bfd12b7edd43092d1b380d70cb2874
|
||||
@@ -0,0 +1,238 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package buildid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errBuildIDMalformed = fmt.Errorf("malformed object file")
|
||||
|
||||
var (
|
||||
bangArch = []byte("!<arch>")
|
||||
pkgdef = []byte("__.PKGDEF")
|
||||
goobject = []byte("go object ")
|
||||
buildid = []byte("build id ")
|
||||
)
|
||||
|
||||
// ReadFile reads the build ID from an archive or executable file.
|
||||
func ReadFile(name string) (id string, err error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := make([]byte, 8)
|
||||
if _, err := f.ReadAt(buf, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if string(buf) != "!<arch>\n" {
|
||||
if string(buf) == "<bigaf>\n" {
|
||||
return "", errors.New("unsupported")
|
||||
}
|
||||
return readBinary(name, f)
|
||||
}
|
||||
|
||||
// Read just enough of the target to fetch the build ID.
|
||||
// The archive is expected to look like:
|
||||
//
|
||||
// !<arch>
|
||||
// __.PKGDEF 0 0 0 644 7955 `
|
||||
// go object darwin amd64 devel X:none
|
||||
// build id "b41e5c45250e25c9fd5e9f9a1de7857ea0d41224"
|
||||
//
|
||||
// The variable-sized strings are GOOS, GOARCH, and the experiment list (X:none).
|
||||
// Reading the first 1024 bytes should be plenty.
|
||||
data := make([]byte, 1024)
|
||||
n, err := io.ReadFull(f, data)
|
||||
if err != nil && n == 0 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tryGccgo := func() (string, error) {
|
||||
return readGccgoArchive(name, f)
|
||||
}
|
||||
|
||||
// Archive header.
|
||||
for i := 0; ; i++ { // returns during i==3
|
||||
j := bytes.IndexByte(data, '\n')
|
||||
if j < 0 {
|
||||
return tryGccgo()
|
||||
}
|
||||
line := data[:j]
|
||||
data = data[j+1:]
|
||||
switch i {
|
||||
case 0:
|
||||
if !bytes.Equal(line, bangArch) {
|
||||
return tryGccgo()
|
||||
}
|
||||
case 1:
|
||||
if !bytes.HasPrefix(line, pkgdef) {
|
||||
return tryGccgo()
|
||||
}
|
||||
case 2:
|
||||
if !bytes.HasPrefix(line, goobject) {
|
||||
return tryGccgo()
|
||||
}
|
||||
case 3:
|
||||
if !bytes.HasPrefix(line, buildid) {
|
||||
// Found the object header, just doesn't have a build id line.
|
||||
// Treat as successful, with empty build id.
|
||||
return "", nil
|
||||
}
|
||||
id, err := strconv.Unquote(string(line[len(buildid):]))
|
||||
if err != nil {
|
||||
return tryGccgo()
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readGccgoArchive tries to parse the archive as a standard Unix
|
||||
// archive file, and fetch the build ID from the _buildid.o entry.
|
||||
// The _buildid.o entry is written by (*Builder).gccgoBuildIDELFFile
|
||||
// in cmd/go/internal/work/exec.go.
|
||||
func readGccgoArchive(name string, f *os.File) (string, error) {
|
||||
bad := func() (string, error) {
|
||||
return "", &os.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
|
||||
}
|
||||
|
||||
off := int64(8)
|
||||
for {
|
||||
if _, err := f.Seek(off, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO(iant): Make a debug/ar package, and use it
|
||||
// here and in cmd/link.
|
||||
var hdr [60]byte
|
||||
if _, err := io.ReadFull(f, hdr[:]); err != nil {
|
||||
if err == io.EOF {
|
||||
// No more entries, no build ID.
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
off += 60
|
||||
|
||||
sizeStr := strings.TrimSpace(string(hdr[48:58]))
|
||||
size, err := strconv.ParseInt(sizeStr, 0, 64)
|
||||
if err != nil {
|
||||
return bad()
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(string(hdr[:16]))
|
||||
if name == "_buildid.o/" {
|
||||
sr := io.NewSectionReader(f, off, size)
|
||||
e, err := elf.NewFile(sr)
|
||||
if err != nil {
|
||||
return bad()
|
||||
}
|
||||
s := e.Section(".go.buildid")
|
||||
if s == nil {
|
||||
return bad()
|
||||
}
|
||||
data, err := s.Data()
|
||||
if err != nil {
|
||||
return bad()
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
off += size
|
||||
if off&1 != 0 {
|
||||
off++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
goBuildPrefix = []byte("\xff Go build ID: \"")
|
||||
goBuildEnd = []byte("\"\n \xff")
|
||||
|
||||
elfPrefix = []byte("\x7fELF")
|
||||
|
||||
machoPrefixes = [][]byte{
|
||||
{0xfe, 0xed, 0xfa, 0xce},
|
||||
{0xfe, 0xed, 0xfa, 0xcf},
|
||||
{0xce, 0xfa, 0xed, 0xfe},
|
||||
{0xcf, 0xfa, 0xed, 0xfe},
|
||||
}
|
||||
)
|
||||
|
||||
var readSize = 32 * 1024 // changed for testing
|
||||
|
||||
// readBinary reads the build ID from a binary.
|
||||
//
|
||||
// ELF binaries store the build ID in a proper PT_NOTE section.
|
||||
//
|
||||
// Other binary formats are not so flexible. For those, the linker
|
||||
// stores the build ID as non-instruction bytes at the very beginning
|
||||
// of the text segment, which should appear near the beginning
|
||||
// of the file. This is clumsy but fairly portable. Custom locations
|
||||
// can be added for other binary types as needed, like we did for ELF.
|
||||
func readBinary(name string, f *os.File) (id string, err error) {
|
||||
// Read the first 32 kB of the binary file.
|
||||
// That should be enough to find the build ID.
|
||||
// In ELF files, the build ID is in the leading headers,
|
||||
// which are typically less than 4 kB, not to mention 32 kB.
|
||||
// In Mach-O files, there's no limit, so we have to parse the file.
|
||||
// On other systems, we're trying to read enough that
|
||||
// we get the beginning of the text segment in the read.
|
||||
// The offset where the text segment begins in a hello
|
||||
// world compiled for each different object format today:
|
||||
//
|
||||
// Plan 9: 0x20
|
||||
// Windows: 0x600
|
||||
//
|
||||
data := make([]byte, readSize)
|
||||
_, err = io.ReadFull(f, data)
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(data, elfPrefix) {
|
||||
return readELF(name, f, data)
|
||||
}
|
||||
for _, m := range machoPrefixes {
|
||||
if bytes.HasPrefix(data, m) {
|
||||
return readMacho(name, f, data)
|
||||
}
|
||||
}
|
||||
return readRaw(name, data)
|
||||
}
|
||||
|
||||
// readRaw finds the raw build ID stored in text segment data.
|
||||
func readRaw(name string, data []byte) (id string, err error) {
|
||||
i := bytes.Index(data, goBuildPrefix)
|
||||
if i < 0 {
|
||||
// Missing. Treat as successful but build ID empty.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
j := bytes.Index(data[i+len(goBuildPrefix):], goBuildEnd)
|
||||
if j < 0 {
|
||||
return "", &os.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
|
||||
}
|
||||
|
||||
quoted := data[i+len(goBuildPrefix)-1 : i+len(goBuildPrefix)+j+1]
|
||||
id, err = strconv.Unquote(string(quoted))
|
||||
if err != nil {
|
||||
return "", &os.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package buildid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"debug/macho"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func readAligned4(r io.Reader, sz int32) ([]byte, error) {
|
||||
full := (sz + 3) &^ 3
|
||||
data := make([]byte, full)
|
||||
_, err := io.ReadFull(r, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = data[:sz]
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func ReadELFNote(filename, name string, typ int32) ([]byte, error) {
|
||||
f, err := elf.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
for _, sect := range f.Sections {
|
||||
if sect.Type != elf.SHT_NOTE {
|
||||
continue
|
||||
}
|
||||
r := sect.Open()
|
||||
for {
|
||||
var namesize, descsize, noteType int32
|
||||
err = binary.Read(r, f.ByteOrder, &namesize)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("read namesize failed: %v", err)
|
||||
}
|
||||
err = binary.Read(r, f.ByteOrder, &descsize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read descsize failed: %v", err)
|
||||
}
|
||||
err = binary.Read(r, f.ByteOrder, ¬eType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read type failed: %v", err)
|
||||
}
|
||||
noteName, err := readAligned4(r, namesize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read name failed: %v", err)
|
||||
}
|
||||
desc, err := readAligned4(r, descsize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read desc failed: %v", err)
|
||||
}
|
||||
if name == string(noteName) && typ == noteType {
|
||||
return desc, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var elfGoNote = []byte("Go\x00\x00")
|
||||
var elfGNUNote = []byte("GNU\x00")
|
||||
|
||||
// The Go build ID is stored in a note described by an ELF PT_NOTE prog
|
||||
// header. The caller has already opened filename, to get f, and read
|
||||
// at least 4 kB out, in data.
|
||||
func readELF(name string, f *os.File, data []byte) (buildid string, err error) {
|
||||
// Assume the note content is in the data, already read.
|
||||
// Rewrite the ELF header to set shnum to 0, so that we can pass
|
||||
// the data to elf.NewFile and it will decode the Prog list but not
|
||||
// try to read the section headers and the string table from disk.
|
||||
// That's a waste of I/O when all we care about is the Prog list
|
||||
// and the one ELF note.
|
||||
switch elf.Class(data[elf.EI_CLASS]) {
|
||||
case elf.ELFCLASS32:
|
||||
data[48] = 0
|
||||
data[49] = 0
|
||||
case elf.ELFCLASS64:
|
||||
data[60] = 0
|
||||
data[61] = 0
|
||||
}
|
||||
|
||||
const elfGoBuildIDTag = 4
|
||||
const gnuBuildIDTag = 3
|
||||
|
||||
ef, err := elf.NewFile(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", &os.PathError{Path: name, Op: "parse", Err: err}
|
||||
}
|
||||
var gnu string
|
||||
for _, p := range ef.Progs {
|
||||
if p.Type != elf.PT_NOTE || p.Filesz < 16 {
|
||||
continue
|
||||
}
|
||||
|
||||
var note []byte
|
||||
if p.Off+p.Filesz < uint64(len(data)) {
|
||||
note = data[p.Off : p.Off+p.Filesz]
|
||||
} else {
|
||||
// For some linkers, such as the Solaris linker,
|
||||
// the buildid may not be found in data (which
|
||||
// likely contains the first 16kB of the file)
|
||||
// or even the first few megabytes of the file
|
||||
// due to differences in note segment placement;
|
||||
// in that case, extract the note data manually.
|
||||
_, err = f.Seek(int64(p.Off), io.SeekStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
note = make([]byte, p.Filesz)
|
||||
_, err = io.ReadFull(f, note)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
filesz := p.Filesz
|
||||
off := p.Off
|
||||
for filesz >= 16 {
|
||||
nameSize := ef.ByteOrder.Uint32(note)
|
||||
valSize := ef.ByteOrder.Uint32(note[4:])
|
||||
tag := ef.ByteOrder.Uint32(note[8:])
|
||||
nname := note[12:16]
|
||||
if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == elfGoBuildIDTag && bytes.Equal(nname, elfGoNote) {
|
||||
return string(note[16 : 16+valSize]), nil
|
||||
}
|
||||
|
||||
if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == gnuBuildIDTag && bytes.Equal(nname, elfGNUNote) {
|
||||
gnu = string(note[16 : 16+valSize])
|
||||
}
|
||||
|
||||
nameSize = (nameSize + 3) &^ 3
|
||||
valSize = (valSize + 3) &^ 3
|
||||
notesz := uint64(12 + nameSize + valSize)
|
||||
if filesz <= notesz {
|
||||
break
|
||||
}
|
||||
off += notesz
|
||||
align := p.Align
|
||||
alignedOff := (off + align - 1) &^ (align - 1)
|
||||
notesz += alignedOff - off
|
||||
off = alignedOff
|
||||
filesz -= notesz
|
||||
note = note[notesz:]
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a Go note, use a GNU note if available.
|
||||
// This is what gccgo uses.
|
||||
if gnu != "" {
|
||||
return gnu, nil
|
||||
}
|
||||
|
||||
// No note. Treat as successful but build ID empty.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// The Go build ID is stored at the beginning of the Mach-O __text segment.
|
||||
// The caller has already opened filename, to get f, and read a few kB out, in data.
|
||||
// Sadly, that's not guaranteed to hold the note, because there is an arbitrary amount
|
||||
// of other junk placed in the file ahead of the main text.
|
||||
func readMacho(name string, f *os.File, data []byte) (buildid string, err error) {
|
||||
// If the data we want has already been read, don't worry about Mach-O parsing.
|
||||
// This is both an optimization and a hedge against the Mach-O parsing failing
|
||||
// in the future due to, for example, the name of the __text section changing.
|
||||
if b, err := readRaw(name, data); b != "" && err == nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
mf, err := macho.NewFile(f)
|
||||
if err != nil {
|
||||
return "", &os.PathError{Path: name, Op: "parse", Err: err}
|
||||
}
|
||||
|
||||
sect := mf.Section("__text")
|
||||
if sect == nil {
|
||||
// Every binary has a __text section. Something is wrong.
|
||||
return "", &os.PathError{Path: name, Op: "parse", Err: fmt.Errorf("cannot find __text section")}
|
||||
}
|
||||
|
||||
// It should be in the first few bytes, but read a lot just in case,
|
||||
// especially given our past problems on OS X with the build ID moving.
|
||||
// There shouldn't be much difference between reading 4kB and 32kB:
|
||||
// the hard part is getting to the data, not transferring it.
|
||||
n := sect.Size
|
||||
if n > uint64(readSize) {
|
||||
n = uint64(readSize)
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
if _, err := f.ReadAt(buf, int64(sect.Offset)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return readRaw(name, buf)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package gcsizes provides a types.Sizes implementation that adheres
|
||||
// to the rules used by the gc compiler.
|
||||
package gcsizes
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
type Sizes struct {
|
||||
WordSize int64
|
||||
MaxAlign int64
|
||||
}
|
||||
|
||||
// ForArch returns a correct Sizes for the given architecture.
|
||||
func ForArch(arch string) *Sizes {
|
||||
wordSize := int64(8)
|
||||
maxAlign := int64(8)
|
||||
switch build.Default.GOARCH {
|
||||
case "386", "arm":
|
||||
wordSize, maxAlign = 4, 4
|
||||
case "amd64p32":
|
||||
wordSize = 4
|
||||
}
|
||||
return &Sizes{WordSize: wordSize, MaxAlign: maxAlign}
|
||||
}
|
||||
|
||||
func (s *Sizes) Alignof(T types.Type) int64 {
|
||||
switch t := T.Underlying().(type) {
|
||||
case *types.Array:
|
||||
return s.Alignof(t.Elem())
|
||||
case *types.Struct:
|
||||
max := int64(1)
|
||||
n := t.NumFields()
|
||||
var fields []*types.Var
|
||||
for i := 0; i < n; i++ {
|
||||
fields = append(fields, t.Field(i))
|
||||
}
|
||||
for _, f := range fields {
|
||||
if a := s.Alignof(f.Type()); a > max {
|
||||
max = a
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
a := s.Sizeof(T) // may be 0
|
||||
if a < 1 {
|
||||
return 1
|
||||
}
|
||||
if a > s.MaxAlign {
|
||||
return s.MaxAlign
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (s *Sizes) Offsetsof(fields []*types.Var) []int64 {
|
||||
offsets := make([]int64, len(fields))
|
||||
var o int64
|
||||
for i, f := range fields {
|
||||
a := s.Alignof(f.Type())
|
||||
o = align(o, a)
|
||||
offsets[i] = o
|
||||
o += s.Sizeof(f.Type())
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
var basicSizes = [...]byte{
|
||||
types.Bool: 1,
|
||||
types.Int8: 1,
|
||||
types.Int16: 2,
|
||||
types.Int32: 4,
|
||||
types.Int64: 8,
|
||||
types.Uint8: 1,
|
||||
types.Uint16: 2,
|
||||
types.Uint32: 4,
|
||||
types.Uint64: 8,
|
||||
types.Float32: 4,
|
||||
types.Float64: 8,
|
||||
types.Complex64: 8,
|
||||
types.Complex128: 16,
|
||||
}
|
||||
|
||||
func (s *Sizes) Sizeof(T types.Type) int64 {
|
||||
switch t := T.Underlying().(type) {
|
||||
case *types.Basic:
|
||||
k := t.Kind()
|
||||
if int(k) < len(basicSizes) {
|
||||
if s := basicSizes[k]; s > 0 {
|
||||
return int64(s)
|
||||
}
|
||||
}
|
||||
if k == types.String {
|
||||
return s.WordSize * 2
|
||||
}
|
||||
case *types.Array:
|
||||
n := t.Len()
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
a := s.Alignof(t.Elem())
|
||||
z := s.Sizeof(t.Elem())
|
||||
return align(z, a)*(n-1) + z
|
||||
case *types.Slice:
|
||||
return s.WordSize * 3
|
||||
case *types.Struct:
|
||||
n := t.NumFields()
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var fields []*types.Var
|
||||
for i := 0; i < n; i++ {
|
||||
fields = append(fields, t.Field(i))
|
||||
}
|
||||
offsets := s.Offsetsof(fields)
|
||||
a := s.Alignof(T)
|
||||
lsz := s.Sizeof(fields[n-1].Type())
|
||||
if lsz == 0 {
|
||||
lsz = 1
|
||||
}
|
||||
z := offsets[n-1] + lsz
|
||||
return align(z, a)
|
||||
case *types.Interface:
|
||||
return s.WordSize * 2
|
||||
}
|
||||
return s.WordSize // catch-all
|
||||
}
|
||||
|
||||
// align returns the smallest y >= x such that y % a == 0.
|
||||
func align(x, a int64) int64 {
|
||||
y := x + a - 1
|
||||
return y - y%a
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
Copyright (c) 2016 Dominik Honnef. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,9 @@
|
||||
This package started as a copy of golang.org/x/tools/go/ssa, imported from an unknown commit in 2016.
|
||||
It has since been heavily modified to match our own needs in an IR.
|
||||
The changes are too many to list here, and it is best to consider this package independent of go/ssa.
|
||||
|
||||
Upstream changes still get applied when they address bugs in portions of code we have inherited.
|
||||
|
||||
The last upstream commit we've looked at was:
|
||||
e854e0228e2ef1cc6e42bbfde1951925096a1272
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package ir_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"honnef.co/go/tools/go/ir"
|
||||
)
|
||||
|
||||
func BenchmarkSSA(b *testing.B) {
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo,
|
||||
Tests: false,
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, "std")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
prog := ir.NewProgram(pkgs[0].Fset, ir.GlobalDebug)
|
||||
seen := map[*packages.Package]struct{}{}
|
||||
var create func(pkg *packages.Package)
|
||||
create = func(pkg *packages.Package) {
|
||||
if _, ok := seen[pkg]; ok {
|
||||
return
|
||||
}
|
||||
seen[pkg] = struct{}{}
|
||||
prog.CreatePackage(pkg.Types, pkg.Syntax, pkg.TypesInfo, true)
|
||||
for _, imp := range pkg.Imports {
|
||||
create(imp)
|
||||
}
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
create(pkg)
|
||||
}
|
||||
prog.Build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// Simple block optimizations to simplify the control flow graph.
|
||||
|
||||
// TODO(adonovan): opt: instead of creating several "unreachable" blocks
|
||||
// per function in the Builder, reuse a single one (e.g. at Blocks[1])
|
||||
// to reduce garbage.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// If true, perform sanity checking and show progress at each
|
||||
// successive iteration of optimizeBlocks. Very verbose.
|
||||
const debugBlockOpt = false
|
||||
|
||||
// markReachable sets Index=-1 for all blocks reachable from b.
|
||||
func markReachable(b *BasicBlock) {
|
||||
b.gaps = -1
|
||||
for _, succ := range b.Succs {
|
||||
if succ.gaps == 0 {
|
||||
markReachable(succ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleteUnreachableBlocks marks all reachable blocks of f and
|
||||
// eliminates (nils) all others, including possibly cyclic subgraphs.
|
||||
func deleteUnreachableBlocks(f *Function) {
|
||||
const white, black = 0, -1
|
||||
// We borrow b.gaps temporarily as the mark bit.
|
||||
for _, b := range f.Blocks {
|
||||
b.gaps = white
|
||||
}
|
||||
markReachable(f.Blocks[0])
|
||||
// In SSI form, we need the exit to be reachable for correct
|
||||
// post-dominance information. In original form, however, we
|
||||
// cannot unconditionally mark it reachable because we won't
|
||||
// be adding fake edges, and this breaks the calculation of
|
||||
// dominance information.
|
||||
markReachable(f.Exit)
|
||||
for i, b := range f.Blocks {
|
||||
if b.gaps == white {
|
||||
for _, c := range b.Succs {
|
||||
if c.gaps == black {
|
||||
c.removePred(b) // delete white->black edge
|
||||
}
|
||||
}
|
||||
if debugBlockOpt {
|
||||
fmt.Fprintln(os.Stderr, "unreachable", b)
|
||||
}
|
||||
f.Blocks[i] = nil // delete b
|
||||
}
|
||||
}
|
||||
f.removeNilBlocks()
|
||||
}
|
||||
|
||||
// jumpThreading attempts to apply simple jump-threading to block b,
|
||||
// in which a->b->c become a->c if b is just a Jump.
|
||||
// The result is true if the optimization was applied.
|
||||
func jumpThreading(f *Function, b *BasicBlock) bool {
|
||||
if b.Index == 0 {
|
||||
return false // don't apply to entry block
|
||||
}
|
||||
if b.Instrs == nil {
|
||||
return false
|
||||
}
|
||||
for _, pred := range b.Preds {
|
||||
switch pred.Control().(type) {
|
||||
case *ConstantSwitch:
|
||||
// don't optimize away the head blocks of switch statements
|
||||
return false
|
||||
}
|
||||
}
|
||||
if _, ok := b.Instrs[0].(*Jump); !ok {
|
||||
return false // not just a jump
|
||||
}
|
||||
c := b.Succs[0]
|
||||
if c == b {
|
||||
return false // don't apply to degenerate jump-to-self.
|
||||
}
|
||||
if c.hasPhi() {
|
||||
return false // not sound without more effort
|
||||
}
|
||||
for j, a := range b.Preds {
|
||||
a.replaceSucc(b, c)
|
||||
|
||||
// If a now has two edges to c, replace its degenerate If by Jump.
|
||||
if len(a.Succs) == 2 && a.Succs[0] == c && a.Succs[1] == c {
|
||||
jump := new(Jump)
|
||||
jump.setBlock(a)
|
||||
a.Instrs[len(a.Instrs)-1] = jump
|
||||
a.Succs = a.Succs[:1]
|
||||
c.removePred(b)
|
||||
} else {
|
||||
if j == 0 {
|
||||
c.replacePred(b, a)
|
||||
} else {
|
||||
c.Preds = append(c.Preds, a)
|
||||
}
|
||||
}
|
||||
|
||||
if debugBlockOpt {
|
||||
fmt.Fprintln(os.Stderr, "jumpThreading", a, b, c)
|
||||
}
|
||||
}
|
||||
f.Blocks[b.Index] = nil // delete b
|
||||
return true
|
||||
}
|
||||
|
||||
// fuseBlocks attempts to apply the block fusion optimization to block
|
||||
// a, in which a->b becomes ab if len(a.Succs)==len(b.Preds)==1.
|
||||
// The result is true if the optimization was applied.
|
||||
func fuseBlocks(f *Function, a *BasicBlock) bool {
|
||||
if len(a.Succs) != 1 {
|
||||
return false
|
||||
}
|
||||
if a.Succs[0] == f.Exit {
|
||||
return false
|
||||
}
|
||||
b := a.Succs[0]
|
||||
if len(b.Preds) != 1 {
|
||||
return false
|
||||
}
|
||||
if _, ok := a.Instrs[len(a.Instrs)-1].(*Panic); ok {
|
||||
// panics aren't simple jumps, they have side effects.
|
||||
return false
|
||||
}
|
||||
|
||||
// Degenerate &&/|| ops may result in a straight-line CFG
|
||||
// containing φ-nodes. (Ideally we'd replace such them with
|
||||
// their sole operand but that requires Referrers, built later.)
|
||||
if b.hasPhi() {
|
||||
return false // not sound without further effort
|
||||
}
|
||||
|
||||
// Eliminate jump at end of A, then copy all of B across.
|
||||
a.Instrs = append(a.Instrs[:len(a.Instrs)-1], b.Instrs...)
|
||||
for _, instr := range b.Instrs {
|
||||
instr.setBlock(a)
|
||||
}
|
||||
|
||||
// A inherits B's successors
|
||||
a.Succs = append(a.succs2[:0], b.Succs...)
|
||||
|
||||
// Fix up Preds links of all successors of B.
|
||||
for _, c := range b.Succs {
|
||||
c.replacePred(b, a)
|
||||
}
|
||||
|
||||
if debugBlockOpt {
|
||||
fmt.Fprintln(os.Stderr, "fuseBlocks", a, b)
|
||||
}
|
||||
|
||||
f.Blocks[b.Index] = nil // delete b
|
||||
return true
|
||||
}
|
||||
|
||||
// optimizeBlocks() performs some simple block optimizations on a
|
||||
// completed function: dead block elimination, block fusion, jump
|
||||
// threading.
|
||||
func optimizeBlocks(f *Function) {
|
||||
if debugBlockOpt {
|
||||
f.WriteTo(os.Stderr)
|
||||
mustSanityCheck(f, nil)
|
||||
}
|
||||
|
||||
deleteUnreachableBlocks(f)
|
||||
|
||||
// Loop until no further progress.
|
||||
changed := true
|
||||
for changed {
|
||||
changed = false
|
||||
|
||||
if debugBlockOpt {
|
||||
f.WriteTo(os.Stderr)
|
||||
mustSanityCheck(f, nil)
|
||||
}
|
||||
|
||||
for _, b := range f.Blocks {
|
||||
// f.Blocks will temporarily contain nils to indicate
|
||||
// deleted blocks; we remove them at the end.
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fuse blocks. b->c becomes bc.
|
||||
if fuseBlocks(f, b) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
// a->b->c becomes a->c if b contains only a Jump.
|
||||
if jumpThreading(f, b) {
|
||||
changed = true
|
||||
continue // (b was disconnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
f.removeNilBlocks()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.17
|
||||
// +build go1.17
|
||||
|
||||
package ir_test
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
)
|
||||
|
||||
func TestBuildPackageGo117(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
importer types.Importer
|
||||
}{
|
||||
{"slice to array pointer", "package p; var s []byte; var _ = (*[4]byte)(s)", nil},
|
||||
{"unsafe slice", `package p; import "unsafe"; var _ = unsafe.Add(nil, 0)`, importer.Default()},
|
||||
{"unsafe add", `package p; import "unsafe"; var _ = unsafe.Slice((*int)(nil), 0)`, importer.Default()},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "p.go", tc.src, parser.ParseComments|parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
files := []*ast.File{f}
|
||||
|
||||
pkg := types.NewPackage("p", "")
|
||||
conf := &types.Config{Importer: tc.importer}
|
||||
if _, _, err := irutil.BuildPackage(conf, fset, pkg, files, ir.SanityCheckFunctions); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//lint:file-ignore SA1019 go/ssa's test suite is built around the deprecated go/loader. We'll leave fixing that to upstream.
|
||||
|
||||
package ir_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
func isEmpty(f *ir.Function) bool { return f.Blocks == nil }
|
||||
|
||||
// Tests that programs partially loaded from gc object files contain
|
||||
// functions with no code for the external portions, but are otherwise ok.
|
||||
func TestBuildPackage(t *testing.T) {
|
||||
input := `
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var t testing.T
|
||||
t.Parallel() // static call to external declared method
|
||||
t.Fail() // static call to promoted external declared method
|
||||
testing.Short() // static call to external package-level function
|
||||
|
||||
var w io.Writer = new(bytes.Buffer)
|
||||
w.Write(nil) // interface invoke of external declared method
|
||||
}
|
||||
`
|
||||
|
||||
// Parse the file.
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "input.go", input, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build an IR program from the parsed file.
|
||||
// Load its dependencies from gc binary export data.
|
||||
mainPkg, _, err := irutil.BuildPackage(&types.Config{Importer: importer.Default()}, fset,
|
||||
types.NewPackage("main", ""), []*ast.File{f}, ir.SanityCheckFunctions)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// The main package, its direct and indirect dependencies are loaded.
|
||||
deps := []string{
|
||||
// directly imported dependencies:
|
||||
"bytes", "io", "testing",
|
||||
// indirect dependencies mentioned by
|
||||
// the direct imports' export data
|
||||
"sync", "unicode", "time",
|
||||
}
|
||||
|
||||
prog := mainPkg.Prog
|
||||
all := prog.AllPackages()
|
||||
if len(all) <= len(deps) {
|
||||
t.Errorf("unexpected set of loaded packages: %q", all)
|
||||
}
|
||||
for _, path := range deps {
|
||||
pkg := prog.ImportedPackage(path)
|
||||
if pkg == nil {
|
||||
t.Errorf("package not loaded: %q", path)
|
||||
continue
|
||||
}
|
||||
|
||||
// External packages should have no function bodies (except for wrappers).
|
||||
isExt := pkg != mainPkg
|
||||
|
||||
// init()
|
||||
if isExt && !isEmpty(pkg.Func("init")) {
|
||||
t.Errorf("external package %s has non-empty init", pkg)
|
||||
} else if !isExt && isEmpty(pkg.Func("init")) {
|
||||
t.Errorf("main package %s has empty init", pkg)
|
||||
}
|
||||
|
||||
for _, mem := range pkg.Members {
|
||||
switch mem := mem.(type) {
|
||||
case *ir.Function:
|
||||
// Functions at package level.
|
||||
if isExt && !isEmpty(mem) {
|
||||
t.Errorf("external function %s is non-empty", mem)
|
||||
} else if !isExt && isEmpty(mem) {
|
||||
t.Errorf("function %s is empty", mem)
|
||||
}
|
||||
|
||||
case *ir.Type:
|
||||
// Methods of named types T.
|
||||
// (In this test, all exported methods belong to *T not T.)
|
||||
if !isExt {
|
||||
t.Fatalf("unexpected name type in main package: %s", mem)
|
||||
}
|
||||
mset := prog.MethodSets.MethodSet(types.NewPointer(mem.Type()))
|
||||
for i, n := 0, mset.Len(); i < n; i++ {
|
||||
m := prog.MethodValue(mset.At(i))
|
||||
// For external types, only synthetic wrappers have code.
|
||||
expExt := m.Synthetic != ir.SyntheticWrapper
|
||||
if expExt && !isEmpty(m) {
|
||||
t.Errorf("external method %s is non-empty: %s",
|
||||
m, m.Synthetic)
|
||||
} else if !expExt && isEmpty(m) {
|
||||
t.Errorf("method function %s is empty: %s",
|
||||
m, m.Synthetic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expectedCallee := []string{
|
||||
"(*testing.T).Parallel",
|
||||
"(*testing.common).Fail",
|
||||
"testing.Short",
|
||||
"N/A",
|
||||
}
|
||||
callNum := 0
|
||||
for _, b := range mainPkg.Func("main").Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
switch instr := instr.(type) {
|
||||
case ir.CallInstruction:
|
||||
call := instr.Common()
|
||||
if want := expectedCallee[callNum]; want != "N/A" {
|
||||
got := call.StaticCallee().String()
|
||||
if want != got {
|
||||
t.Errorf("call #%d from main.main: got callee %s, want %s",
|
||||
callNum, got, want)
|
||||
}
|
||||
}
|
||||
callNum++
|
||||
}
|
||||
}
|
||||
}
|
||||
if callNum != 4 {
|
||||
t.Errorf("in main.main: got %d calls, want %d", callNum, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeTypes tests that (*Program).RuntimeTypes() includes all necessary types.
|
||||
func TestRuntimeTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
// An exported package-level type is needed.
|
||||
{`package A; type T struct{}; func (T) f() {}`,
|
||||
[]string{"*p.T", "p.T"},
|
||||
},
|
||||
// An unexported package-level type is not needed.
|
||||
{`package B; type t struct{}; func (t) f() {}`,
|
||||
nil,
|
||||
},
|
||||
// Subcomponents of type of exported package-level var are needed.
|
||||
{`package C; import "bytes"; var V struct {*bytes.Buffer}`,
|
||||
[]string{"*bytes.Buffer", "*struct{*bytes.Buffer}", "struct{*bytes.Buffer}"},
|
||||
},
|
||||
// Subcomponents of type of unexported package-level var are not needed.
|
||||
{`package D; import "bytes"; var v struct {*bytes.Buffer}`,
|
||||
nil,
|
||||
},
|
||||
// Subcomponents of type of exported package-level function are needed.
|
||||
{`package E; import "bytes"; func F(struct {*bytes.Buffer}) {}`,
|
||||
[]string{"*bytes.Buffer", "struct{*bytes.Buffer}"},
|
||||
},
|
||||
// Subcomponents of type of unexported package-level function are not needed.
|
||||
{`package F; import "bytes"; func f(struct {*bytes.Buffer}) {}`,
|
||||
nil,
|
||||
},
|
||||
// Subcomponents of type of exported method of uninstantiated unexported type are not needed.
|
||||
{`package G; import "bytes"; type x struct{}; func (x) G(struct {*bytes.Buffer}) {}; var v x`,
|
||||
nil,
|
||||
},
|
||||
// ...unless used by MakeInterface.
|
||||
{`package G2; import "bytes"; type x struct{}; func (x) G(struct {*bytes.Buffer}) {}; var v interface{} = x{}`,
|
||||
[]string{"*bytes.Buffer", "*p.x", "p.x", "struct{*bytes.Buffer}"},
|
||||
},
|
||||
// Subcomponents of type of unexported method are not needed.
|
||||
{`package I; import "bytes"; type X struct{}; func (X) G(struct {*bytes.Buffer}) {}`,
|
||||
[]string{"*bytes.Buffer", "*p.X", "p.X", "struct{*bytes.Buffer}"},
|
||||
},
|
||||
// Local types aren't needed.
|
||||
{`package J; import "bytes"; func f() { type T struct {*bytes.Buffer}; var t T; _ = t }`,
|
||||
nil,
|
||||
},
|
||||
// ...unless used by MakeInterface.
|
||||
{`package K; import "bytes"; func f() { type T struct {*bytes.Buffer}; _ = interface{}(T{}) }`,
|
||||
[]string{"*bytes.Buffer", "*p.T", "p.T"},
|
||||
},
|
||||
// Types used as operand of MakeInterface are needed.
|
||||
{`package L; import "bytes"; func f() { _ = interface{}(struct{*bytes.Buffer}{}) }`,
|
||||
[]string{"*bytes.Buffer", "struct{*bytes.Buffer}"},
|
||||
},
|
||||
// MakeInterface is optimized away when storing to a blank.
|
||||
{`package M; import "bytes"; var _ interface{} = struct{*bytes.Buffer}{}`,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
// Parse the file.
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "input.go", test.input, 0)
|
||||
if err != nil {
|
||||
t.Errorf("test %q: %s", test.input[:15], err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a single-file main package.
|
||||
// Load dependencies from gc binary export data.
|
||||
irpkg, _, err := irutil.BuildPackage(&types.Config{Importer: importer.Default()}, fset,
|
||||
types.NewPackage("p", ""), []*ast.File{f}, ir.SanityCheckFunctions)
|
||||
if err != nil {
|
||||
t.Errorf("test %q: %s", test.input[:15], err)
|
||||
continue
|
||||
}
|
||||
|
||||
var typstrs []string
|
||||
for _, T := range irpkg.Prog.RuntimeTypes() {
|
||||
typstrs = append(typstrs, T.String())
|
||||
}
|
||||
sort.Strings(typstrs)
|
||||
|
||||
if !reflect.DeepEqual(typstrs, test.want) {
|
||||
t.Errorf("test 'package %s': got %q, want %q",
|
||||
f.Name.Name, typstrs, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInit tests that synthesized init functions are correctly formed.
|
||||
func TestInit(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode ir.BuilderMode
|
||||
input, want string
|
||||
}{
|
||||
{0, `package A; import _ "errors"; var i int = 42`,
|
||||
`# Name: A.init
|
||||
# Package: A
|
||||
# Synthetic: package initializer
|
||||
func init():
|
||||
b0: # entry
|
||||
t1 = Const <bool> {true}
|
||||
t2 = Const <int> {42}
|
||||
t3 = Load <bool> init$guard
|
||||
If t3 → b1 b2
|
||||
|
||||
b1: ← b0 b2 # exit
|
||||
Return
|
||||
|
||||
b2: ← b0 # init.start
|
||||
Store {bool} init$guard t1
|
||||
t7 = Call <()> errors.init
|
||||
Store {int} i t2
|
||||
Jump → b1
|
||||
|
||||
`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
// Create a single-file main package.
|
||||
var conf loader.Config
|
||||
f, err := conf.ParseFile("<input>", test.input)
|
||||
if err != nil {
|
||||
t.Errorf("test %q: %s", test.input[:15], err)
|
||||
continue
|
||||
}
|
||||
conf.CreateFromFiles(f.Name.Name, f)
|
||||
|
||||
lprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Errorf("test 'package %s': Load: %s", f.Name.Name, err)
|
||||
continue
|
||||
}
|
||||
prog := irutil.CreateProgram(lprog, test.mode)
|
||||
mainPkg := prog.Package(lprog.Created[0].Pkg)
|
||||
prog.Build()
|
||||
initFunc := mainPkg.Func("init")
|
||||
if initFunc == nil {
|
||||
t.Errorf("test 'package %s': no init function", f.Name.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
var initbuf bytes.Buffer
|
||||
_, err = initFunc.WriteTo(&initbuf)
|
||||
if err != nil {
|
||||
t.Errorf("test 'package %s': WriteTo: %s", f.Name.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if initbuf.String() != test.want {
|
||||
t.Errorf("test 'package %s': got %s, want %s", f.Name.Name, initbuf.String(), test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSyntheticFuncs checks that the expected synthetic functions are
|
||||
// created, reachable, and not duplicated.
|
||||
func TestSyntheticFuncs(t *testing.T) {
|
||||
const input = `package P
|
||||
type T int
|
||||
func (T) f() int
|
||||
func (*T) g() int
|
||||
var (
|
||||
// thunks
|
||||
a = T.f
|
||||
b = T.f
|
||||
c = (struct{T}).f
|
||||
d = (struct{T}).f
|
||||
e = (*T).g
|
||||
f = (*T).g
|
||||
g = (struct{*T}).g
|
||||
h = (struct{*T}).g
|
||||
|
||||
// bounds
|
||||
i = T(0).f
|
||||
j = T(0).f
|
||||
k = new(T).g
|
||||
l = new(T).g
|
||||
|
||||
// wrappers
|
||||
m interface{} = struct{T}{}
|
||||
n interface{} = struct{T}{}
|
||||
o interface{} = struct{*T}{}
|
||||
p interface{} = struct{*T}{}
|
||||
q interface{} = new(struct{T})
|
||||
r interface{} = new(struct{T})
|
||||
s interface{} = new(struct{*T})
|
||||
t interface{} = new(struct{*T})
|
||||
)
|
||||
`
|
||||
// Parse
|
||||
var conf loader.Config
|
||||
f, err := conf.ParseFile("<input>", input)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
conf.CreateFromFiles(f.Name.Name, f)
|
||||
|
||||
// Load
|
||||
lprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
// Create and build IR
|
||||
prog := irutil.CreateProgram(lprog, 0)
|
||||
prog.Build()
|
||||
|
||||
// Enumerate reachable synthetic functions
|
||||
want := map[string]ir.Synthetic{
|
||||
"(*P.T).g$bound": ir.SyntheticBound,
|
||||
"(P.T).f$bound": ir.SyntheticBound,
|
||||
|
||||
"(*P.T).g$thunk": ir.SyntheticThunk,
|
||||
"(P.T).f$thunk": ir.SyntheticThunk,
|
||||
"(struct{*P.T}).g$thunk": ir.SyntheticThunk,
|
||||
"(struct{P.T}).f$thunk": ir.SyntheticThunk,
|
||||
|
||||
"(*P.T).f": ir.SyntheticWrapper,
|
||||
"(*struct{*P.T}).f": ir.SyntheticWrapper,
|
||||
"(*struct{*P.T}).g": ir.SyntheticWrapper,
|
||||
"(*struct{P.T}).f": ir.SyntheticWrapper,
|
||||
"(*struct{P.T}).g": ir.SyntheticWrapper,
|
||||
"(struct{*P.T}).f": ir.SyntheticWrapper,
|
||||
"(struct{*P.T}).g": ir.SyntheticWrapper,
|
||||
"(struct{P.T}).f": ir.SyntheticWrapper,
|
||||
|
||||
"P.init": ir.SyntheticPackageInitializer,
|
||||
}
|
||||
for fn := range irutil.AllFunctions(prog) {
|
||||
if fn.Synthetic == 0 {
|
||||
continue
|
||||
}
|
||||
name := fn.String()
|
||||
wantDescr, ok := want[name]
|
||||
if !ok {
|
||||
t.Errorf("got unexpected/duplicate func: %q: %q", name, fn.Synthetic)
|
||||
continue
|
||||
}
|
||||
delete(want, name)
|
||||
|
||||
if wantDescr != fn.Synthetic {
|
||||
t.Errorf("(%s).Synthetic = %q, want %q", name, fn.Synthetic, wantDescr)
|
||||
}
|
||||
}
|
||||
for fn, descr := range want {
|
||||
t.Errorf("want func: %q: %q", fn, descr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhiElimination ensures that dead phis, including those that
|
||||
// participate in a cycle, are properly eliminated.
|
||||
func TestPhiElimination(t *testing.T) {
|
||||
const input = `
|
||||
package p
|
||||
|
||||
func f() error
|
||||
|
||||
func g(slice []int) {
|
||||
for {
|
||||
for range slice {
|
||||
// e should not be lifted to a dead φ-node.
|
||||
e := f()
|
||||
h(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func h(error)
|
||||
`
|
||||
// The IR code for this function should look something like this:
|
||||
// 0:
|
||||
// jump 1
|
||||
// 1:
|
||||
// t0 = len(slice)
|
||||
// jump 2
|
||||
// 2:
|
||||
// t1 = phi [1: -1:int, 3: t2]
|
||||
// t2 = t1 + 1:int
|
||||
// t3 = t2 < t0
|
||||
// if t3 goto 3 else 1
|
||||
// 3:
|
||||
// t4 = f()
|
||||
// t5 = h(t4)
|
||||
// jump 2
|
||||
//
|
||||
// But earlier versions of the IR construction algorithm would
|
||||
// additionally generate this cycle of dead phis:
|
||||
//
|
||||
// 1:
|
||||
// t7 = phi [0: nil:error, 2: t8] #e
|
||||
// ...
|
||||
// 2:
|
||||
// t8 = phi [1: t7, 3: t4] #e
|
||||
// ...
|
||||
|
||||
// Parse
|
||||
var conf loader.Config
|
||||
f, err := conf.ParseFile("<input>", input)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
conf.CreateFromFiles("p", f)
|
||||
|
||||
// Load
|
||||
lprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
// Create and build IR
|
||||
prog := irutil.CreateProgram(lprog, 0)
|
||||
p := prog.Package(lprog.Package("p").Pkg)
|
||||
p.Build()
|
||||
g := p.Func("g")
|
||||
|
||||
phis := 0
|
||||
for _, b := range g.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
if _, ok := instr.(*ir.Phi); ok {
|
||||
phis++
|
||||
}
|
||||
}
|
||||
}
|
||||
if expected := 4; phis != expected {
|
||||
g.WriteTo(os.Stderr)
|
||||
t.Errorf("expected %d Phi nodes (for the range index, slice length and slice), got %d", expected, phis)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file defines the Const SSA value type.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/constant"
|
||||
"go/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/typeparams"
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// NewConst returns a new constant of the specified value and type.
|
||||
// val must be valid according to the specification of Const.Value.
|
||||
func NewConst(val constant.Value, typ types.Type) *Const {
|
||||
return &Const{
|
||||
register: register{
|
||||
typ: typ,
|
||||
},
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// intConst returns an 'int' constant that evaluates to i.
|
||||
// (i is an int64 in case the host is narrower than the target.)
|
||||
func intConst(i int64) *Const {
|
||||
return NewConst(constant.MakeInt64(i), tInt)
|
||||
}
|
||||
|
||||
// nilConst returns a nil constant of the specified type, which may
|
||||
// be any reference type, including interfaces.
|
||||
func nilConst(typ types.Type) *Const {
|
||||
return NewConst(nil, typ)
|
||||
}
|
||||
|
||||
// stringConst returns a 'string' constant that evaluates to s.
|
||||
func stringConst(s string) *Const {
|
||||
return NewConst(constant.MakeString(s), tString)
|
||||
}
|
||||
|
||||
// zeroConst returns a new "zero" constant of the specified type.
|
||||
func zeroConst(t types.Type) Constant {
|
||||
if _, ok := t.Underlying().(*types.Interface); ok && !typeparams.IsTypeParam(t) {
|
||||
// Handle non-generic interface early to simplify following code.
|
||||
return nilConst(t)
|
||||
}
|
||||
|
||||
tset := typeutil.NewTypeSet(t)
|
||||
|
||||
switch typ := tset.CoreType().(type) {
|
||||
case *types.Struct:
|
||||
values := make([]Value, typ.NumFields())
|
||||
for i := 0; i < typ.NumFields(); i++ {
|
||||
values[i] = zeroConst(typ.Field(i).Type())
|
||||
}
|
||||
return &AggregateConst{
|
||||
register: register{typ: t},
|
||||
Values: values,
|
||||
}
|
||||
case *types.Tuple:
|
||||
values := make([]Value, typ.Len())
|
||||
for i := 0; i < typ.Len(); i++ {
|
||||
values[i] = zeroConst(typ.At(i).Type())
|
||||
}
|
||||
return &AggregateConst{
|
||||
register: register{typ: t},
|
||||
Values: values,
|
||||
}
|
||||
}
|
||||
|
||||
isNillable := func(term *types.Term) bool {
|
||||
switch typ := term.Type().Underlying().(type) {
|
||||
case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature, *typeutil.Iterator:
|
||||
return true
|
||||
case *types.Basic:
|
||||
switch typ.Kind() {
|
||||
case types.UnsafePointer, types.UntypedNil:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
isInfo := func(info types.BasicInfo) func(*types.Term) bool {
|
||||
return func(term *types.Term) bool {
|
||||
basic, ok := term.Type().Underlying().(*types.Basic)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return (basic.Info() & info) != 0
|
||||
}
|
||||
}
|
||||
|
||||
isArray := func(term *types.Term) bool {
|
||||
_, ok := term.Type().Underlying().(*types.Array)
|
||||
return ok
|
||||
}
|
||||
|
||||
switch {
|
||||
case tset.All(isInfo(types.IsNumeric)):
|
||||
return NewConst(constant.MakeInt64(0), t)
|
||||
case tset.All(isInfo(types.IsString)):
|
||||
return NewConst(constant.MakeString(""), t)
|
||||
case tset.All(isInfo(types.IsBoolean)):
|
||||
return NewConst(constant.MakeBool(false), t)
|
||||
case tset.All(isNillable):
|
||||
return nilConst(t)
|
||||
case tset.All(isArray):
|
||||
var k ArrayConst
|
||||
k.setType(t)
|
||||
return &k
|
||||
default:
|
||||
var k GenericConst
|
||||
k.setType(t)
|
||||
return &k
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Const) RelString(from *types.Package) string {
|
||||
var p string
|
||||
if c.Value == nil {
|
||||
p = "nil"
|
||||
} else if c.Value.Kind() == constant.String {
|
||||
v := constant.StringVal(c.Value)
|
||||
const max = 20
|
||||
// TODO(adonovan): don't cut a rune in half.
|
||||
if len(v) > max {
|
||||
v = v[:max-3] + "..." // abbreviate
|
||||
}
|
||||
p = strconv.Quote(v)
|
||||
} else {
|
||||
p = c.Value.String()
|
||||
}
|
||||
return fmt.Sprintf("Const <%s> {%s}", relType(c.Type(), from), p)
|
||||
}
|
||||
|
||||
func (c *Const) String() string {
|
||||
if c.block == nil {
|
||||
// Constants don't have a block till late in the compilation process. But we want to print consts during
|
||||
// debugging.
|
||||
return c.RelString(nil)
|
||||
}
|
||||
return c.RelString(c.Parent().pkg())
|
||||
}
|
||||
|
||||
func (v *ArrayConst) RelString(pkg *types.Package) string {
|
||||
return fmt.Sprintf("ArrayConst <%s>", relType(v.Type(), pkg))
|
||||
}
|
||||
|
||||
func (v *ArrayConst) String() string {
|
||||
return v.RelString(v.Parent().pkg())
|
||||
}
|
||||
|
||||
func (v *AggregateConst) RelString(pkg *types.Package) string {
|
||||
values := make([]string, len(v.Values))
|
||||
for i, v := range v.Values {
|
||||
if v != nil {
|
||||
values[i] = v.Name()
|
||||
} else {
|
||||
values[i] = "nil"
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("AggregateConst <%s> (%s)", relType(v.Type(), pkg), strings.Join(values, ", "))
|
||||
}
|
||||
|
||||
func (v *AggregateConst) String() string {
|
||||
if v.block == nil {
|
||||
return v.RelString(nil)
|
||||
}
|
||||
return v.RelString(v.Parent().pkg())
|
||||
}
|
||||
|
||||
func (v *GenericConst) RelString(pkg *types.Package) string {
|
||||
return fmt.Sprintf("GenericConst <%s>", relType(v.Type(), pkg))
|
||||
}
|
||||
|
||||
func (v *GenericConst) String() string {
|
||||
return v.RelString(v.Parent().pkg())
|
||||
}
|
||||
|
||||
// IsNil returns true if this constant represents a typed or untyped nil value.
|
||||
func (c *Const) IsNil() bool {
|
||||
return c.Value == nil
|
||||
}
|
||||
|
||||
// Int64 returns the numeric value of this constant truncated to fit
|
||||
// a signed 64-bit integer.
|
||||
func (c *Const) Int64() int64 {
|
||||
switch x := constant.ToInt(c.Value); x.Kind() {
|
||||
case constant.Int:
|
||||
if i, ok := constant.Int64Val(x); ok {
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
case constant.Float:
|
||||
f, _ := constant.Float64Val(x)
|
||||
return int64(f)
|
||||
}
|
||||
panic(fmt.Sprintf("unexpected constant value: %T", c.Value))
|
||||
}
|
||||
|
||||
// Uint64 returns the numeric value of this constant truncated to fit
|
||||
// an unsigned 64-bit integer.
|
||||
func (c *Const) Uint64() uint64 {
|
||||
switch x := constant.ToInt(c.Value); x.Kind() {
|
||||
case constant.Int:
|
||||
if u, ok := constant.Uint64Val(x); ok {
|
||||
return u
|
||||
}
|
||||
return 0
|
||||
case constant.Float:
|
||||
f, _ := constant.Float64Val(x)
|
||||
return uint64(f)
|
||||
}
|
||||
panic(fmt.Sprintf("unexpected constant value: %T", c.Value))
|
||||
}
|
||||
|
||||
// Float64 returns the numeric value of this constant truncated to fit
|
||||
// a float64.
|
||||
func (c *Const) Float64() float64 {
|
||||
f, _ := constant.Float64Val(c.Value)
|
||||
return f
|
||||
}
|
||||
|
||||
// Complex128 returns the complex value of this constant truncated to
|
||||
// fit a complex128.
|
||||
func (c *Const) Complex128() complex128 {
|
||||
re, _ := constant.Float64Val(constant.Real(c.Value))
|
||||
im, _ := constant.Float64Val(constant.Imag(c.Value))
|
||||
return complex(re, im)
|
||||
}
|
||||
|
||||
func (c *Const) equal(o Constant) bool {
|
||||
// TODO(dh): don't use == for types, this will miss identical pointer types, among others
|
||||
oc, ok := o.(*Const)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return c.typ == oc.typ && c.Value == oc.Value
|
||||
}
|
||||
|
||||
func (c *AggregateConst) equal(o Constant) bool {
|
||||
oc, ok := o.(*AggregateConst)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// TODO(dh): don't use == for types, this will miss identical pointer types, among others
|
||||
if c.typ != oc.typ {
|
||||
return false
|
||||
}
|
||||
for i, v := range c.Values {
|
||||
if !v.(Constant).equal(oc.Values[i].(Constant)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ArrayConst) equal(o Constant) bool {
|
||||
oc, ok := o.(*ArrayConst)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// TODO(dh): don't use == for types, this will miss identical pointer types, among others
|
||||
return c.typ == oc.typ
|
||||
}
|
||||
|
||||
func (c *GenericConst) equal(o Constant) bool {
|
||||
oc, ok := o.(*GenericConst)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// TODO(dh): don't use == for types, this will miss identical pointer types, among others
|
||||
return c.typ == oc.typ
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file implements the CREATE phase of IR construction.
|
||||
// See builder.go for explanation.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// measured on the standard library and rounded up to powers of two,
|
||||
// on average there are 8 blocks and 16 instructions per block in a
|
||||
// function.
|
||||
const avgBlocks = 8
|
||||
const avgInstructionsPerBlock = 16
|
||||
|
||||
// NewProgram returns a new IR Program.
|
||||
//
|
||||
// mode controls diagnostics and checking during IR construction.
|
||||
func NewProgram(fset *token.FileSet, mode BuilderMode) *Program {
|
||||
prog := &Program{
|
||||
Fset: fset,
|
||||
imported: make(map[string]*Package),
|
||||
packages: make(map[*types.Package]*Package),
|
||||
thunks: make(map[selectionKey]*Function),
|
||||
bounds: make(map[*types.Func]*Function),
|
||||
mode: mode,
|
||||
}
|
||||
|
||||
h := typeutil.MakeHasher() // protected by methodsMu, in effect
|
||||
prog.methodSets.SetHasher(h)
|
||||
prog.canon.SetHasher(h)
|
||||
|
||||
return prog
|
||||
}
|
||||
|
||||
// memberFromObject populates package pkg with a member for the
|
||||
// typechecker object obj.
|
||||
//
|
||||
// For objects from Go source code, syntax is the associated syntax
|
||||
// tree (for funcs and vars only); it will be used during the build
|
||||
// phase.
|
||||
func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) {
|
||||
name := obj.Name()
|
||||
switch obj := obj.(type) {
|
||||
case *types.Builtin:
|
||||
if pkg.Pkg != types.Unsafe {
|
||||
panic("unexpected builtin object: " + obj.String())
|
||||
}
|
||||
|
||||
case *types.TypeName:
|
||||
pkg.Members[name] = &Type{
|
||||
object: obj,
|
||||
pkg: pkg,
|
||||
}
|
||||
|
||||
case *types.Const:
|
||||
c := &NamedConst{
|
||||
object: obj,
|
||||
Value: NewConst(obj.Val(), obj.Type()),
|
||||
pkg: pkg,
|
||||
}
|
||||
pkg.values[obj] = c.Value
|
||||
pkg.Members[name] = c
|
||||
|
||||
case *types.Var:
|
||||
g := &Global{
|
||||
Pkg: pkg,
|
||||
name: name,
|
||||
object: obj,
|
||||
typ: types.NewPointer(obj.Type()), // address
|
||||
}
|
||||
pkg.values[obj] = g
|
||||
pkg.Members[name] = g
|
||||
|
||||
case *types.Func:
|
||||
sig := obj.Type().(*types.Signature)
|
||||
if sig.Recv() == nil && name == "init" {
|
||||
pkg.ninit++
|
||||
name = fmt.Sprintf("init#%d", pkg.ninit)
|
||||
}
|
||||
fn := &Function{
|
||||
name: name,
|
||||
object: obj,
|
||||
Signature: sig,
|
||||
Pkg: pkg,
|
||||
Prog: pkg.Prog,
|
||||
}
|
||||
|
||||
fn.source = syntax
|
||||
fn.initHTML(pkg.printFunc)
|
||||
if syntax == nil {
|
||||
fn.Synthetic = SyntheticLoadedFromExportData
|
||||
} else {
|
||||
// Note: we initialize fn.Blocks in
|
||||
// (*builder).buildFunction and not here because Blocks
|
||||
// being nil is used to indicate that building of the
|
||||
// function hasn't started yet.
|
||||
|
||||
fn.functionBody = &functionBody{
|
||||
scratchInstructions: make([]Instruction, avgBlocks*avgInstructionsPerBlock),
|
||||
}
|
||||
}
|
||||
|
||||
pkg.values[obj] = fn
|
||||
pkg.Functions = append(pkg.Functions, fn)
|
||||
if sig.Recv() == nil {
|
||||
pkg.Members[name] = fn // package-level function
|
||||
}
|
||||
|
||||
default: // (incl. *types.Package)
|
||||
panic("unexpected Object type: " + obj.String())
|
||||
}
|
||||
}
|
||||
|
||||
// membersFromDecl populates package pkg with members for each
|
||||
// typechecker object (var, func, const or type) associated with the
|
||||
// specified decl.
|
||||
func membersFromDecl(pkg *Package, decl ast.Decl) {
|
||||
switch decl := decl.(type) {
|
||||
case *ast.GenDecl: // import, const, type or var
|
||||
switch decl.Tok {
|
||||
case token.CONST:
|
||||
for _, spec := range decl.Specs {
|
||||
for _, id := range spec.(*ast.ValueSpec).Names {
|
||||
if !isBlankIdent(id) {
|
||||
memberFromObject(pkg, pkg.info.Defs[id], nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case token.VAR:
|
||||
for _, spec := range decl.Specs {
|
||||
for _, id := range spec.(*ast.ValueSpec).Names {
|
||||
if !isBlankIdent(id) {
|
||||
memberFromObject(pkg, pkg.info.Defs[id], spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case token.TYPE:
|
||||
for _, spec := range decl.Specs {
|
||||
id := spec.(*ast.TypeSpec).Name
|
||||
if !isBlankIdent(id) {
|
||||
memberFromObject(pkg, pkg.info.Defs[id], nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.FuncDecl:
|
||||
id := decl.Name
|
||||
if !isBlankIdent(id) {
|
||||
memberFromObject(pkg, pkg.info.Defs[id], decl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePackage constructs and returns an IR Package from the
|
||||
// specified type-checked, error-free file ASTs, and populates its
|
||||
// Members mapping.
|
||||
//
|
||||
// importable determines whether this package should be returned by a
|
||||
// subsequent call to ImportedPackage(pkg.Path()).
|
||||
//
|
||||
// The real work of building IR form for each function is not done
|
||||
// until a subsequent call to Package.Build().
|
||||
func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info *types.Info, importable bool) *Package {
|
||||
p := &Package{
|
||||
Prog: prog,
|
||||
Members: make(map[string]Member),
|
||||
values: make(map[types.Object]Value),
|
||||
Pkg: pkg,
|
||||
info: info, // transient (CREATE and BUILD phases)
|
||||
files: files, // transient (CREATE and BUILD phases)
|
||||
printFunc: prog.PrintFunc,
|
||||
}
|
||||
|
||||
// Add init() function.
|
||||
p.init = &Function{
|
||||
name: "init",
|
||||
Signature: new(types.Signature),
|
||||
Synthetic: SyntheticPackageInitializer,
|
||||
Pkg: p,
|
||||
Prog: prog,
|
||||
functionBody: new(functionBody),
|
||||
}
|
||||
p.init.initHTML(prog.PrintFunc)
|
||||
p.Members[p.init.name] = p.init
|
||||
p.Functions = append(p.Functions, p.init)
|
||||
|
||||
// CREATE phase.
|
||||
// Allocate all package members: vars, funcs, consts and types.
|
||||
if len(files) > 0 {
|
||||
// Go source package.
|
||||
for _, file := range files {
|
||||
for _, decl := range file.Decls {
|
||||
membersFromDecl(p, decl)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// GC-compiled binary package (or "unsafe")
|
||||
// No code.
|
||||
// No position information.
|
||||
scope := p.Pkg.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
obj := scope.Lookup(name)
|
||||
memberFromObject(p, obj, nil)
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
if named, ok := obj.Type().(*types.Named); ok {
|
||||
for i, n := 0, named.NumMethods(); i < n; i++ {
|
||||
memberFromObject(p, named.Method(i), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add initializer guard variable.
|
||||
initguard := &Global{
|
||||
Pkg: p,
|
||||
name: "init$guard",
|
||||
typ: types.NewPointer(tBool),
|
||||
}
|
||||
p.Members[initguard.Name()] = initguard
|
||||
|
||||
if prog.mode&GlobalDebug != 0 {
|
||||
p.SetDebugMode(true)
|
||||
}
|
||||
|
||||
if prog.mode&PrintPackages != 0 {
|
||||
printMu.Lock()
|
||||
p.WriteTo(os.Stdout)
|
||||
printMu.Unlock()
|
||||
}
|
||||
|
||||
if importable {
|
||||
prog.imported[p.Pkg.Path()] = p
|
||||
}
|
||||
prog.packages[p.Pkg] = p
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// printMu serializes printing of Packages/Functions to stdout.
|
||||
var printMu sync.Mutex
|
||||
|
||||
// AllPackages returns a new slice containing all packages in the
|
||||
// program prog in unspecified order.
|
||||
func (prog *Program) AllPackages() []*Package {
|
||||
pkgs := make([]*Package, 0, len(prog.packages))
|
||||
for _, pkg := range prog.packages {
|
||||
pkgs = append(pkgs, pkg)
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// ImportedPackage returns the importable Package whose PkgPath
|
||||
// is path, or nil if no such Package has been created.
|
||||
//
|
||||
// A parameter to CreatePackage determines whether a package should be
|
||||
// considered importable. For example, no import declaration can resolve
|
||||
// to the ad-hoc main package created by 'go build foo.go'.
|
||||
//
|
||||
// TODO(adonovan): rethink this function and the "importable" concept;
|
||||
// most packages are importable. This function assumes that all
|
||||
// types.Package.Path values are unique within the ir.Program, which is
|
||||
// false---yet this function remains very convenient.
|
||||
// Clients should use (*Program).Package instead where possible.
|
||||
// IR doesn't really need a string-keyed map of packages.
|
||||
func (prog *Program) ImportedPackage(path string) *Package {
|
||||
return prog.imported[path]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ir defines a representation of the elements of Go programs
|
||||
// (packages, types, functions, variables and constants) using a
|
||||
// static single-information (SSI) form intermediate representation
|
||||
// (IR) for the bodies of functions.
|
||||
//
|
||||
// THIS INTERFACE IS EXPERIMENTAL AND IS LIKELY TO CHANGE.
|
||||
//
|
||||
// For an introduction to SSA form, upon which SSI builds, see
|
||||
// http://en.wikipedia.org/wiki/Static_single_assignment_form.
|
||||
// This page provides a broader reading list:
|
||||
// http://www.dcs.gla.ac.uk/~jsinger/ssa.html.
|
||||
//
|
||||
// For an introduction to SSI form, see The static single information
|
||||
// form by C. Scott Ananian.
|
||||
//
|
||||
// The level of abstraction of the IR form is intentionally close to
|
||||
// the source language to facilitate construction of source analysis
|
||||
// tools. It is not intended for machine code generation.
|
||||
//
|
||||
// The simplest way to create the IR of a package is
|
||||
// to load typed syntax trees using golang.org/x/tools/go/packages, then
|
||||
// invoke the irutil.Packages helper function. See ExampleLoadPackages
|
||||
// and ExampleWholeProgram for examples.
|
||||
// The resulting ir.Program contains all the packages and their
|
||||
// members, but IR code is not created for function bodies until a
|
||||
// subsequent call to (*Package).Build or (*Program).Build.
|
||||
//
|
||||
// The builder initially builds a naive IR form in which all local
|
||||
// variables are addresses of stack locations with explicit loads and
|
||||
// stores. Registerization of eligible locals and φ-node insertion
|
||||
// using dominance and dataflow are then performed as a second pass
|
||||
// called "lifting" to improve the accuracy and performance of
|
||||
// subsequent analyses; this pass can be skipped by setting the
|
||||
// NaiveForm builder flag.
|
||||
//
|
||||
// The primary interfaces of this package are:
|
||||
//
|
||||
// - Member: a named member of a Go package.
|
||||
// - Value: an expression that yields a value.
|
||||
// - Instruction: a statement that consumes values and performs computation.
|
||||
// - Node: a Value or Instruction (emphasizing its membership in the IR value graph)
|
||||
//
|
||||
// A computation that yields a result implements both the Value and
|
||||
// Instruction interfaces. The following table shows for each
|
||||
// concrete type which of these interfaces it implements.
|
||||
//
|
||||
// Value? Instruction? Member?
|
||||
// *Alloc ✔ ✔
|
||||
// *BinOp ✔ ✔
|
||||
// *BlankStore ✔
|
||||
// *Builtin ✔
|
||||
// *Call ✔ ✔
|
||||
// *ChangeInterface ✔ ✔
|
||||
// *ChangeType ✔ ✔
|
||||
// *Const ✔ ✔
|
||||
// *Convert ✔ ✔
|
||||
// *DebugRef ✔
|
||||
// *Defer ✔ ✔
|
||||
// *Extract ✔ ✔
|
||||
// *Field ✔ ✔
|
||||
// *FieldAddr ✔ ✔
|
||||
// *FreeVar ✔
|
||||
// *Function ✔ ✔ (func)
|
||||
// *Global ✔ ✔ (var)
|
||||
// *Go ✔ ✔
|
||||
// *If ✔
|
||||
// *Index ✔ ✔
|
||||
// *IndexAddr ✔ ✔
|
||||
// *Jump ✔
|
||||
// *Load ✔ ✔
|
||||
// *MakeChan ✔ ✔
|
||||
// *MakeClosure ✔ ✔
|
||||
// *MakeInterface ✔ ✔
|
||||
// *MakeMap ✔ ✔
|
||||
// *MakeSlice ✔ ✔
|
||||
// *MapLookup ✔ ✔
|
||||
// *MapUpdate ✔ ✔
|
||||
// *NamedConst ✔ (const)
|
||||
// *Next ✔ ✔
|
||||
// *Panic ✔
|
||||
// *Parameter ✔ ✔
|
||||
// *Phi ✔ ✔
|
||||
// *Range ✔ ✔
|
||||
// *Recv ✔ ✔
|
||||
// *Return ✔
|
||||
// *RunDefers ✔
|
||||
// *Select ✔ ✔
|
||||
// *Send ✔ ✔
|
||||
// *Sigma ✔ ✔
|
||||
// *Slice ✔ ✔
|
||||
// *SliceToArrayPointer ✔ ✔
|
||||
// *SliceToArray ✔ ✔
|
||||
// *Store ✔ ✔
|
||||
// *StringLookup ✔ ✔
|
||||
// *Type ✔ (type)
|
||||
// *TypeAssert ✔ ✔
|
||||
// *UnOp ✔ ✔
|
||||
// *Unreachable ✔
|
||||
//
|
||||
// Other key types in this package include: Program, Package, Function
|
||||
// and BasicBlock.
|
||||
//
|
||||
// The program representation constructed by this package is fully
|
||||
// resolved internally, i.e. it does not rely on the names of Values,
|
||||
// Packages, Functions, Types or BasicBlocks for the correct
|
||||
// interpretation of the program. Only the identities of objects and
|
||||
// the topology of the IR and type graphs are semantically
|
||||
// significant. (There is one exception: Ids, used to identify field
|
||||
// and method names, contain strings.) Avoidance of name-based
|
||||
// operations simplifies the implementation of subsequent passes and
|
||||
// can make them very efficient. Many objects are nonetheless named
|
||||
// to aid in debugging, but it is not essential that the names be
|
||||
// either accurate or unambiguous. The public API exposes a number of
|
||||
// name-based maps for client convenience.
|
||||
//
|
||||
// The ir/irutil package provides various utilities that depend only
|
||||
// on the public API of this package.
|
||||
//
|
||||
// TODO(adonovan): Consider the exceptional control-flow implications
|
||||
// of defer and recover().
|
||||
//
|
||||
// TODO(adonovan): write a how-to document for all the various cases
|
||||
// of trying to determine corresponding elements across the four
|
||||
// domains of source locations, ast.Nodes, types.Objects,
|
||||
// ir.Values/Instructions.
|
||||
package ir
|
||||
@@ -0,0 +1,466 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file defines algorithms related to dominance.
|
||||
|
||||
// Dominator tree construction ----------------------------------------
|
||||
//
|
||||
// We use the algorithm described in Lengauer & Tarjan. 1979. A fast
|
||||
// algorithm for finding dominators in a flowgraph.
|
||||
// http://doi.acm.org/10.1145/357062.357071
|
||||
//
|
||||
// We also apply the optimizations to SLT described in Georgiadis et
|
||||
// al, Finding Dominators in Practice, JGAA 2006,
|
||||
// http://jgaa.info/accepted/2006/GeorgiadisTarjanWerneck2006.10.1.pdf
|
||||
// to avoid the need for buckets of size > 1.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Idom returns the block that immediately dominates b:
|
||||
// its parent in the dominator tree, if any.
|
||||
// The entry node (b.Index==0) does not have a parent.
|
||||
func (b *BasicBlock) Idom() *BasicBlock { return b.dom.idom }
|
||||
|
||||
// Dominees returns the list of blocks that b immediately dominates:
|
||||
// its children in the dominator tree.
|
||||
func (b *BasicBlock) Dominees() []*BasicBlock { return b.dom.children }
|
||||
|
||||
// Dominates reports whether b dominates c.
|
||||
func (b *BasicBlock) Dominates(c *BasicBlock) bool {
|
||||
return b.dom.pre <= c.dom.pre && c.dom.post <= b.dom.post
|
||||
}
|
||||
|
||||
type byDomPreorder []*BasicBlock
|
||||
|
||||
func (a byDomPreorder) Len() int { return len(a) }
|
||||
func (a byDomPreorder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byDomPreorder) Less(i, j int) bool { return a[i].dom.pre < a[j].dom.pre }
|
||||
|
||||
// DomPreorder returns a new slice containing the blocks of f in
|
||||
// dominator tree preorder.
|
||||
func (f *Function) DomPreorder() []*BasicBlock {
|
||||
n := len(f.Blocks)
|
||||
order := make(byDomPreorder, n)
|
||||
copy(order, f.Blocks)
|
||||
sort.Sort(order)
|
||||
return order
|
||||
}
|
||||
|
||||
// domInfo contains a BasicBlock's dominance information.
|
||||
type domInfo struct {
|
||||
idom *BasicBlock // immediate dominator (parent in domtree)
|
||||
children []*BasicBlock // nodes immediately dominated by this one
|
||||
pre, post int32 // pre- and post-order numbering within domtree
|
||||
}
|
||||
|
||||
// buildDomTree computes the dominator tree of f using the LT algorithm.
|
||||
// Precondition: all blocks are reachable (e.g. optimizeBlocks has been run).
|
||||
func buildDomTree(fn *Function) {
|
||||
// The step numbers refer to the original LT paper; the
|
||||
// reordering is due to Georgiadis.
|
||||
|
||||
// Clear any previous domInfo.
|
||||
for _, b := range fn.Blocks {
|
||||
b.dom = domInfo{}
|
||||
}
|
||||
|
||||
idoms := make([]*BasicBlock, len(fn.Blocks))
|
||||
|
||||
order := make([]*BasicBlock, 0, len(fn.Blocks))
|
||||
seen := fn.blockset(0)
|
||||
var dfs func(b *BasicBlock)
|
||||
dfs = func(b *BasicBlock) {
|
||||
if !seen.Add(b) {
|
||||
return
|
||||
}
|
||||
for _, succ := range b.Succs {
|
||||
dfs(succ)
|
||||
}
|
||||
if fn.fakeExits.Has(b) {
|
||||
dfs(fn.Exit)
|
||||
}
|
||||
order = append(order, b)
|
||||
b.post = len(order) - 1
|
||||
}
|
||||
dfs(fn.Blocks[0])
|
||||
|
||||
for i := 0; i < len(order)/2; i++ {
|
||||
o := len(order) - i - 1
|
||||
order[i], order[o] = order[o], order[i]
|
||||
}
|
||||
|
||||
idoms[fn.Blocks[0].Index] = fn.Blocks[0]
|
||||
changed := true
|
||||
for changed {
|
||||
changed = false
|
||||
// iterate over all nodes in reverse postorder, except for the
|
||||
// entry node
|
||||
for _, b := range order[1:] {
|
||||
var newIdom *BasicBlock
|
||||
do := func(p *BasicBlock) {
|
||||
if idoms[p.Index] == nil {
|
||||
return
|
||||
}
|
||||
if newIdom == nil {
|
||||
newIdom = p
|
||||
} else {
|
||||
finger1 := p
|
||||
finger2 := newIdom
|
||||
for finger1 != finger2 {
|
||||
for finger1.post < finger2.post {
|
||||
finger1 = idoms[finger1.Index]
|
||||
}
|
||||
for finger2.post < finger1.post {
|
||||
finger2 = idoms[finger2.Index]
|
||||
}
|
||||
}
|
||||
newIdom = finger1
|
||||
}
|
||||
}
|
||||
for _, p := range b.Preds {
|
||||
do(p)
|
||||
}
|
||||
if b == fn.Exit {
|
||||
for _, p := range fn.Blocks {
|
||||
if fn.fakeExits.Has(p) {
|
||||
do(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if idoms[b.Index] != newIdom {
|
||||
idoms[b.Index] = newIdom
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range idoms {
|
||||
fn.Blocks[i].dom.idom = b
|
||||
if b == nil {
|
||||
// malformed CFG
|
||||
continue
|
||||
}
|
||||
if i == b.Index {
|
||||
continue
|
||||
}
|
||||
b.dom.children = append(b.dom.children, fn.Blocks[i])
|
||||
}
|
||||
|
||||
numberDomTree(fn.Blocks[0], 0, 0)
|
||||
|
||||
// printDomTreeDot(os.Stderr, fn) // debugging
|
||||
// printDomTreeText(os.Stderr, root, 0) // debugging
|
||||
|
||||
if fn.Prog.mode&SanityCheckFunctions != 0 {
|
||||
sanityCheckDomTree(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// buildPostDomTree is like buildDomTree, but builds the post-dominator tree instead.
|
||||
func buildPostDomTree(fn *Function) {
|
||||
// The step numbers refer to the original LT paper; the
|
||||
// reordering is due to Georgiadis.
|
||||
|
||||
// Clear any previous domInfo.
|
||||
for _, b := range fn.Blocks {
|
||||
b.pdom = domInfo{}
|
||||
}
|
||||
|
||||
idoms := make([]*BasicBlock, len(fn.Blocks))
|
||||
|
||||
order := make([]*BasicBlock, 0, len(fn.Blocks))
|
||||
seen := fn.blockset(0)
|
||||
var dfs func(b *BasicBlock)
|
||||
dfs = func(b *BasicBlock) {
|
||||
if !seen.Add(b) {
|
||||
return
|
||||
}
|
||||
for _, pred := range b.Preds {
|
||||
dfs(pred)
|
||||
}
|
||||
if b == fn.Exit {
|
||||
for _, p := range fn.Blocks {
|
||||
if fn.fakeExits.Has(p) {
|
||||
dfs(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
order = append(order, b)
|
||||
b.post = len(order) - 1
|
||||
}
|
||||
dfs(fn.Exit)
|
||||
|
||||
for i := 0; i < len(order)/2; i++ {
|
||||
o := len(order) - i - 1
|
||||
order[i], order[o] = order[o], order[i]
|
||||
}
|
||||
|
||||
idoms[fn.Exit.Index] = fn.Exit
|
||||
changed := true
|
||||
for changed {
|
||||
changed = false
|
||||
// iterate over all nodes in reverse postorder, except for the
|
||||
// exit node
|
||||
for _, b := range order[1:] {
|
||||
var newIdom *BasicBlock
|
||||
do := func(p *BasicBlock) {
|
||||
if idoms[p.Index] == nil {
|
||||
return
|
||||
}
|
||||
if newIdom == nil {
|
||||
newIdom = p
|
||||
} else {
|
||||
finger1 := p
|
||||
finger2 := newIdom
|
||||
for finger1 != finger2 {
|
||||
for finger1.post < finger2.post {
|
||||
finger1 = idoms[finger1.Index]
|
||||
}
|
||||
for finger2.post < finger1.post {
|
||||
finger2 = idoms[finger2.Index]
|
||||
}
|
||||
}
|
||||
newIdom = finger1
|
||||
}
|
||||
}
|
||||
for _, p := range b.Succs {
|
||||
do(p)
|
||||
}
|
||||
if fn.fakeExits.Has(b) {
|
||||
do(fn.Exit)
|
||||
}
|
||||
|
||||
if idoms[b.Index] != newIdom {
|
||||
idoms[b.Index] = newIdom
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range idoms {
|
||||
fn.Blocks[i].pdom.idom = b
|
||||
if b == nil {
|
||||
// malformed CFG
|
||||
continue
|
||||
}
|
||||
if i == b.Index {
|
||||
continue
|
||||
}
|
||||
b.pdom.children = append(b.pdom.children, fn.Blocks[i])
|
||||
}
|
||||
|
||||
numberPostDomTree(fn.Exit, 0, 0)
|
||||
|
||||
// printPostDomTreeDot(os.Stderr, fn) // debugging
|
||||
// printPostDomTreeText(os.Stderr, fn.Exit, 0) // debugging
|
||||
|
||||
if fn.Prog.mode&SanityCheckFunctions != 0 { // XXX
|
||||
sanityCheckDomTree(fn) // XXX
|
||||
}
|
||||
}
|
||||
|
||||
// numberDomTree sets the pre- and post-order numbers of a depth-first
|
||||
// traversal of the dominator tree rooted at v. These are used to
|
||||
// answer dominance queries in constant time.
|
||||
func numberDomTree(v *BasicBlock, pre, post int32) (int32, int32) {
|
||||
v.dom.pre = pre
|
||||
pre++
|
||||
for _, child := range v.dom.children {
|
||||
pre, post = numberDomTree(child, pre, post)
|
||||
}
|
||||
v.dom.post = post
|
||||
post++
|
||||
return pre, post
|
||||
}
|
||||
|
||||
// numberPostDomTree sets the pre- and post-order numbers of a depth-first
|
||||
// traversal of the post-dominator tree rooted at v. These are used to
|
||||
// answer post-dominance queries in constant time.
|
||||
func numberPostDomTree(v *BasicBlock, pre, post int32) (int32, int32) {
|
||||
v.pdom.pre = pre
|
||||
pre++
|
||||
for _, child := range v.pdom.children {
|
||||
pre, post = numberPostDomTree(child, pre, post)
|
||||
}
|
||||
v.pdom.post = post
|
||||
post++
|
||||
return pre, post
|
||||
}
|
||||
|
||||
// Testing utilities ----------------------------------------
|
||||
|
||||
// sanityCheckDomTree checks the correctness of the dominator tree
|
||||
// computed by the LT algorithm by comparing against the dominance
|
||||
// relation computed by a naive Kildall-style forward dataflow
|
||||
// analysis (Algorithm 10.16 from the "Dragon" book).
|
||||
func sanityCheckDomTree(f *Function) {
|
||||
n := len(f.Blocks)
|
||||
|
||||
// D[i] is the set of blocks that dominate f.Blocks[i],
|
||||
// represented as a bit-set of block indices.
|
||||
D := make([]big.Int, n)
|
||||
|
||||
one := big.NewInt(1)
|
||||
|
||||
// all is the set of all blocks; constant.
|
||||
var all big.Int
|
||||
all.Set(one).Lsh(&all, uint(n)).Sub(&all, one)
|
||||
|
||||
// Initialization.
|
||||
for i := range f.Blocks {
|
||||
if i == 0 {
|
||||
// A root is dominated only by itself.
|
||||
D[i].SetBit(&D[0], 0, 1)
|
||||
} else {
|
||||
// All other blocks are (initially) dominated
|
||||
// by every block.
|
||||
D[i].Set(&all)
|
||||
}
|
||||
}
|
||||
|
||||
// Iteration until fixed point.
|
||||
for changed := true; changed; {
|
||||
changed = false
|
||||
for i, b := range f.Blocks {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
// Compute intersection across predecessors.
|
||||
var x big.Int
|
||||
x.Set(&all)
|
||||
for _, pred := range b.Preds {
|
||||
x.And(&x, &D[pred.Index])
|
||||
}
|
||||
if b == f.Exit {
|
||||
for _, p := range f.Blocks {
|
||||
if f.fakeExits.Has(p) {
|
||||
x.And(&x, &D[p.Index])
|
||||
}
|
||||
}
|
||||
}
|
||||
x.SetBit(&x, i, 1) // a block always dominates itself.
|
||||
if D[i].Cmp(&x) != 0 {
|
||||
D[i].Set(&x)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the entire relation. O(n^2).
|
||||
ok := true
|
||||
for i := 0; i < n; i++ {
|
||||
for j := 0; j < n; j++ {
|
||||
b, c := f.Blocks[i], f.Blocks[j]
|
||||
actual := b.Dominates(c)
|
||||
expected := D[j].Bit(i) == 1
|
||||
if actual != expected {
|
||||
fmt.Fprintf(os.Stderr, "dominates(%s, %s)==%t, want %t\n", b, c, actual, expected)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preorder := f.DomPreorder()
|
||||
for _, b := range f.Blocks {
|
||||
if got := preorder[b.dom.pre]; got != b {
|
||||
fmt.Fprintf(os.Stderr, "preorder[%d]==%s, want %s\n", b.dom.pre, got, b)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
panic("sanityCheckDomTree failed for " + f.String())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Printing functions ----------------------------------------
|
||||
|
||||
// printDomTree prints the dominator tree as text, using indentation.
|
||||
//
|
||||
//lint:ignore U1000 used during debugging
|
||||
func printDomTreeText(buf *bytes.Buffer, v *BasicBlock, indent int) {
|
||||
fmt.Fprintf(buf, "%*s%s\n", 4*indent, "", v)
|
||||
for _, child := range v.dom.children {
|
||||
printDomTreeText(buf, child, indent+1)
|
||||
}
|
||||
}
|
||||
|
||||
// printDomTreeDot prints the dominator tree of f in AT&T GraphViz
|
||||
// (.dot) format.
|
||||
//
|
||||
//lint:ignore U1000 used during debugging
|
||||
func printDomTreeDot(buf io.Writer, f *Function) {
|
||||
fmt.Fprintln(buf, "//", f)
|
||||
fmt.Fprintln(buf, "digraph domtree {")
|
||||
for i, b := range f.Blocks {
|
||||
v := b.dom
|
||||
fmt.Fprintf(buf, "\tn%d [label=\"%s (%d, %d)\",shape=\"rectangle\"];\n", v.pre, b, v.pre, v.post)
|
||||
// TODO(adonovan): improve appearance of edges
|
||||
// belonging to both dominator tree and CFG.
|
||||
|
||||
// Dominator tree edge.
|
||||
if i != 0 {
|
||||
fmt.Fprintf(buf, "\tn%d -> n%d [style=\"solid\",weight=100];\n", v.idom.dom.pre, v.pre)
|
||||
}
|
||||
// CFG edges.
|
||||
for _, pred := range b.Preds {
|
||||
fmt.Fprintf(buf, "\tn%d -> n%d [style=\"dotted\",weight=0];\n", pred.dom.pre, v.pre)
|
||||
}
|
||||
|
||||
if f.fakeExits.Has(b) {
|
||||
fmt.Fprintf(buf, "\tn%d -> n%d [style=\"dotted\",weight=0,color=red];\n", b.dom.pre, f.Exit.dom.pre)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(buf, "}")
|
||||
}
|
||||
|
||||
// printDomTree prints the dominator tree as text, using indentation.
|
||||
//
|
||||
//lint:ignore U1000 used during debugging
|
||||
func printPostDomTreeText(buf io.Writer, v *BasicBlock, indent int) {
|
||||
fmt.Fprintf(buf, "%*s%s\n", 4*indent, "", v)
|
||||
for _, child := range v.pdom.children {
|
||||
printPostDomTreeText(buf, child, indent+1)
|
||||
}
|
||||
}
|
||||
|
||||
// printDomTreeDot prints the dominator tree of f in AT&T GraphViz
|
||||
// (.dot) format.
|
||||
//
|
||||
//lint:ignore U1000 used during debugging
|
||||
func printPostDomTreeDot(buf io.Writer, f *Function) {
|
||||
fmt.Fprintln(buf, "//", f)
|
||||
fmt.Fprintln(buf, "digraph pdomtree {")
|
||||
for _, b := range f.Blocks {
|
||||
v := b.pdom
|
||||
fmt.Fprintf(buf, "\tn%d [label=\"%s (%d, %d)\",shape=\"rectangle\"];\n", v.pre, b, v.pre, v.post)
|
||||
// TODO(adonovan): improve appearance of edges
|
||||
// belonging to both dominator tree and CFG.
|
||||
|
||||
// Dominator tree edge.
|
||||
if b != f.Exit {
|
||||
fmt.Fprintf(buf, "\tn%d -> n%d [style=\"solid\",weight=100];\n", v.idom.pdom.pre, v.pre)
|
||||
}
|
||||
// CFG edges.
|
||||
for _, pred := range b.Preds {
|
||||
fmt.Fprintf(buf, "\tn%d -> n%d [style=\"dotted\",weight=0];\n", pred.pdom.pre, v.pre)
|
||||
}
|
||||
|
||||
if f.fakeExits.Has(b) {
|
||||
fmt.Fprintf(buf, "\tn%d -> n%d [style=\"dotted\",weight=0,color=red];\n", b.dom.pre, f.Exit.dom.pre)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(buf, "}")
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// Helpers for emitting IR instructions.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
|
||||
"golang.org/x/exp/typeparams"
|
||||
)
|
||||
|
||||
// emitNew emits to f a new (heap Alloc) instruction allocating an
|
||||
// object of type typ. pos is the optional source location.
|
||||
func emitNew(f *Function, typ types.Type, source ast.Node) *Alloc {
|
||||
v := &Alloc{Heap: true}
|
||||
v.setType(types.NewPointer(typ))
|
||||
f.emit(v, source)
|
||||
return v
|
||||
}
|
||||
|
||||
// emitLoad emits to f an instruction to load the address addr into a
|
||||
// new temporary, and returns the value so defined.
|
||||
func emitLoad(f *Function, addr Value, source ast.Node) *Load {
|
||||
v := &Load{X: addr}
|
||||
v.setType(deref(addr.Type()))
|
||||
f.emit(v, source)
|
||||
return v
|
||||
}
|
||||
|
||||
func emitRecv(f *Function, ch Value, commaOk bool, typ types.Type, source ast.Node) Value {
|
||||
recv := &Recv{
|
||||
Chan: ch,
|
||||
CommaOk: commaOk,
|
||||
}
|
||||
recv.setType(typ)
|
||||
return f.emit(recv, source)
|
||||
}
|
||||
|
||||
// emitDebugRef emits to f a DebugRef pseudo-instruction associating
|
||||
// expression e with value v.
|
||||
func emitDebugRef(f *Function, e ast.Expr, v Value, isAddr bool) {
|
||||
ref := makeDebugRef(f, e, v, isAddr)
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
f.emit(ref, nil)
|
||||
}
|
||||
|
||||
func makeDebugRef(f *Function, e ast.Expr, v Value, isAddr bool) *DebugRef {
|
||||
if !f.debugInfo() {
|
||||
return nil // debugging not enabled
|
||||
}
|
||||
if v == nil || e == nil {
|
||||
panic("nil")
|
||||
}
|
||||
var obj types.Object
|
||||
e = unparen(e)
|
||||
if id, ok := e.(*ast.Ident); ok {
|
||||
if isBlankIdent(id) {
|
||||
return nil
|
||||
}
|
||||
obj = f.Pkg.objectOf(id)
|
||||
switch obj.(type) {
|
||||
case *types.Nil, *types.Const, *types.Builtin:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &DebugRef{
|
||||
X: v,
|
||||
Expr: e,
|
||||
IsAddr: isAddr,
|
||||
object: obj,
|
||||
}
|
||||
}
|
||||
|
||||
// emitArith emits to f code to compute the binary operation op(x, y)
|
||||
// where op is an eager shift, logical or arithmetic operation.
|
||||
// (Use emitCompare() for comparisons and Builder.logicalBinop() for
|
||||
// non-eager operations.)
|
||||
func emitArith(f *Function, op token.Token, x, y Value, t types.Type, source ast.Node) Value {
|
||||
switch op {
|
||||
case token.SHL, token.SHR:
|
||||
x = emitConv(f, x, t, source)
|
||||
// y may be signed or an 'untyped' constant.
|
||||
// There is a runtime panic if y is signed and <0. Instead of inserting a check for y<0
|
||||
// and converting to an unsigned value (like the compiler) leave y as is.
|
||||
if b, ok := y.Type().Underlying().(*types.Basic); ok && b.Info()&types.IsUntyped != 0 {
|
||||
// Untyped conversion:
|
||||
// Spec https://go.dev/ref/spec#Operators:
|
||||
// The right operand in a shift expression must have integer type or be an untyped constant
|
||||
// representable by a value of type uint.
|
||||
y = emitConv(f, y, types.Typ[types.Uint], source)
|
||||
}
|
||||
|
||||
case token.ADD, token.SUB, token.MUL, token.QUO, token.REM, token.AND, token.OR, token.XOR, token.AND_NOT:
|
||||
x = emitConv(f, x, t, source)
|
||||
y = emitConv(f, y, t, source)
|
||||
|
||||
default:
|
||||
panic("illegal op in emitArith: " + op.String())
|
||||
|
||||
}
|
||||
v := &BinOp{
|
||||
Op: op,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
v.setType(t)
|
||||
return f.emit(v, source)
|
||||
}
|
||||
|
||||
// emitCompare emits to f code compute the boolean result of
|
||||
// comparison comparison 'x op y'.
|
||||
func emitCompare(f *Function, op token.Token, x, y Value, source ast.Node) Value {
|
||||
xt := x.Type().Underlying()
|
||||
yt := y.Type().Underlying()
|
||||
|
||||
// Special case to optimise a tagless SwitchStmt so that
|
||||
// these are equivalent
|
||||
// switch { case e: ...}
|
||||
// switch true { case e: ... }
|
||||
// if e==true { ... }
|
||||
// even in the case when e's type is an interface.
|
||||
// TODO(adonovan): opt: generalise to x==true, false!=y, etc.
|
||||
if x, ok := x.(*Const); ok && op == token.EQL && x.Value != nil && x.Value.Kind() == constant.Bool && constant.BoolVal(x.Value) {
|
||||
if yt, ok := yt.(*types.Basic); ok && yt.Info()&types.IsBoolean != 0 {
|
||||
return y
|
||||
}
|
||||
}
|
||||
|
||||
if types.Identical(xt, yt) {
|
||||
// no conversion necessary
|
||||
} else if _, ok := xt.(*types.Interface); ok && !typeparams.IsTypeParam(x.Type()) {
|
||||
y = emitConv(f, y, x.Type(), source)
|
||||
} else if _, ok := yt.(*types.Interface); ok && !typeparams.IsTypeParam(y.Type()) {
|
||||
x = emitConv(f, x, y.Type(), source)
|
||||
} else if _, ok := x.(*Const); ok {
|
||||
x = emitConv(f, x, y.Type(), source)
|
||||
} else if _, ok := y.(*Const); ok {
|
||||
y = emitConv(f, y, x.Type(), source)
|
||||
//lint:ignore SA9003 no-op
|
||||
} else {
|
||||
// other cases, e.g. channels. No-op.
|
||||
}
|
||||
|
||||
v := &BinOp{
|
||||
Op: op,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
v.setType(tBool)
|
||||
return f.emit(v, source)
|
||||
}
|
||||
|
||||
// isValuePreserving returns true if a conversion from ut_src to
|
||||
// ut_dst is value-preserving, i.e. just a change of type.
|
||||
// Precondition: neither argument is a named type.
|
||||
func isValuePreserving(ut_src, ut_dst types.Type) bool {
|
||||
// Identical underlying types?
|
||||
if types.IdenticalIgnoreTags(ut_dst, ut_src) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch ut_dst.(type) {
|
||||
case *types.Chan:
|
||||
// Conversion between channel types?
|
||||
_, ok := ut_src.(*types.Chan)
|
||||
return ok
|
||||
|
||||
case *types.Pointer:
|
||||
// Conversion between pointers with identical base types?
|
||||
_, ok := ut_src.(*types.Pointer)
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// emitConv emits to f code to convert Value val to exactly type typ,
|
||||
// and returns the converted value. Implicit conversions are required
|
||||
// by language assignability rules in assignments, parameter passing,
|
||||
// etc.
|
||||
func emitConv(f *Function, val Value, t_dst types.Type, source ast.Node) Value {
|
||||
t_src := val.Type()
|
||||
|
||||
// Identical types? Conversion is a no-op.
|
||||
if types.Identical(t_src, t_dst) {
|
||||
return val
|
||||
}
|
||||
|
||||
ut_dst := t_dst.Underlying()
|
||||
ut_src := t_src.Underlying()
|
||||
|
||||
tset_dst := typeutil.NewTypeSet(ut_dst)
|
||||
tset_src := typeutil.NewTypeSet(ut_src)
|
||||
|
||||
// Just a change of type, but not value or representation?
|
||||
if tset_src.All(func(termSrc *types.Term) bool {
|
||||
return tset_dst.All(func(termDst *types.Term) bool {
|
||||
return isValuePreserving(termSrc.Type().Underlying(), termDst.Type().Underlying())
|
||||
})
|
||||
}) {
|
||||
c := &ChangeType{X: val}
|
||||
c.setType(t_dst)
|
||||
return f.emit(c, source)
|
||||
}
|
||||
|
||||
// Conversion to, or construction of a value of, an interface type?
|
||||
if _, ok := ut_dst.(*types.Interface); ok && !typeparams.IsTypeParam(t_dst) {
|
||||
// Assignment from one interface type to another?
|
||||
if _, ok := ut_src.(*types.Interface); ok && !typeparams.IsTypeParam(t_src) {
|
||||
c := &ChangeInterface{X: val}
|
||||
c.setType(t_dst)
|
||||
return f.emit(c, source)
|
||||
}
|
||||
|
||||
// Untyped nil constant? Return interface-typed nil constant.
|
||||
if ut_src == tUntypedNil {
|
||||
return emitConst(f, nilConst(t_dst))
|
||||
}
|
||||
|
||||
// Convert (non-nil) "untyped" literals to their default type.
|
||||
if t, ok := ut_src.(*types.Basic); ok && t.Info()&types.IsUntyped != 0 {
|
||||
val = emitConv(f, val, types.Default(ut_src), source)
|
||||
}
|
||||
|
||||
f.Pkg.Prog.needMethodsOf(val.Type())
|
||||
mi := &MakeInterface{X: val}
|
||||
mi.setType(t_dst)
|
||||
return f.emit(mi, source)
|
||||
}
|
||||
|
||||
// Conversion of a compile-time constant value? Note that converting a constant to a type parameter never results in
|
||||
// a constant value.
|
||||
if c, ok := val.(*Const); ok {
|
||||
if _, ok := ut_dst.(*types.Basic); ok || c.IsNil() {
|
||||
// Conversion of a compile-time constant to
|
||||
// another constant type results in a new
|
||||
// constant of the destination type and
|
||||
// (initially) the same abstract value.
|
||||
// We don't truncate the value yet.
|
||||
return emitConst(f, NewConst(c.Value, t_dst))
|
||||
}
|
||||
|
||||
// We're converting from constant to non-constant type,
|
||||
// e.g. string -> []byte/[]rune.
|
||||
}
|
||||
|
||||
// Conversion from slice to array pointer?
|
||||
if tset_src.All(func(termSrc *types.Term) bool {
|
||||
return tset_dst.All(func(termDst *types.Term) bool {
|
||||
if slice, ok := termSrc.Type().Underlying().(*types.Slice); ok {
|
||||
if ptr, ok := termDst.Type().Underlying().(*types.Pointer); ok {
|
||||
if arr, ok := ptr.Elem().Underlying().(*types.Array); ok && types.Identical(slice.Elem(), arr.Elem()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}) {
|
||||
c := &SliceToArrayPointer{X: val}
|
||||
c.setType(t_dst)
|
||||
return f.emit(c, source)
|
||||
}
|
||||
|
||||
// Conversion from slice to array. This is almost the same as converting from slice to array pointer, then
|
||||
// dereferencing the pointer. Except that a nil slice can be converted to [0]T, whereas converting a nil slice to
|
||||
// (*[0]T) results in a nil pointer, dereferencing which would panic. To hide the extra branching we use a dedicated
|
||||
// instruction, SliceToArray.
|
||||
if tset_src.All(func(termSrc *types.Term) bool {
|
||||
return tset_dst.All(func(termDst *types.Term) bool {
|
||||
if slice, ok := termSrc.Type().Underlying().(*types.Slice); ok {
|
||||
if arr, ok := termDst.Type().Underlying().(*types.Array); ok && types.Identical(slice.Elem(), arr.Elem()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}) {
|
||||
c := &SliceToArray{X: val}
|
||||
c.setType(t_dst)
|
||||
return f.emit(c, source)
|
||||
}
|
||||
|
||||
// A representation-changing conversion?
|
||||
// At least one of {ut_src,ut_dst} must be *Basic.
|
||||
// (The other may be []byte or []rune.)
|
||||
ok1 := tset_src.Any(func(term *types.Term) bool { _, ok := term.Type().Underlying().(*types.Basic); return ok })
|
||||
ok2 := tset_dst.Any(func(term *types.Term) bool { _, ok := term.Type().Underlying().(*types.Basic); return ok })
|
||||
if ok1 || ok2 {
|
||||
c := &Convert{X: val}
|
||||
c.setType(t_dst)
|
||||
return f.emit(c, source)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("in %s: cannot convert %s (%s) to %s", f, val, val.Type(), t_dst))
|
||||
}
|
||||
|
||||
// emitStore emits to f an instruction to store value val at location
|
||||
// addr, applying implicit conversions as required by assignability rules.
|
||||
func emitStore(f *Function, addr, val Value, source ast.Node) *Store {
|
||||
s := &Store{
|
||||
Addr: addr,
|
||||
Val: emitConv(f, val, deref(addr.Type()), source),
|
||||
}
|
||||
f.emit(s, source)
|
||||
return s
|
||||
}
|
||||
|
||||
// emitJump emits to f a jump to target, and updates the control-flow graph.
|
||||
// Postcondition: f.currentBlock is nil.
|
||||
func emitJump(f *Function, target *BasicBlock, source ast.Node) *Jump {
|
||||
b := f.currentBlock
|
||||
j := new(Jump)
|
||||
b.emit(j, source)
|
||||
addEdge(b, target)
|
||||
f.currentBlock = nil
|
||||
return j
|
||||
}
|
||||
|
||||
// emitIf emits to f a conditional jump to tblock or fblock based on
|
||||
// cond, and updates the control-flow graph.
|
||||
// Postcondition: f.currentBlock is nil.
|
||||
func emitIf(f *Function, cond Value, tblock, fblock *BasicBlock, source ast.Node) *If {
|
||||
b := f.currentBlock
|
||||
stmt := &If{Cond: cond}
|
||||
b.emit(stmt, source)
|
||||
addEdge(b, tblock)
|
||||
addEdge(b, fblock)
|
||||
f.currentBlock = nil
|
||||
return stmt
|
||||
}
|
||||
|
||||
// emitExtract emits to f an instruction to extract the index'th
|
||||
// component of tuple. It returns the extracted value.
|
||||
func emitExtract(f *Function, tuple Value, index int, source ast.Node) Value {
|
||||
e := &Extract{Tuple: tuple, Index: index}
|
||||
e.setType(tuple.Type().(*types.Tuple).At(index).Type())
|
||||
return f.emit(e, source)
|
||||
}
|
||||
|
||||
// emitTypeAssert emits to f a type assertion value := x.(t) and
|
||||
// returns the value. x.Type() must be an interface.
|
||||
func emitTypeAssert(f *Function, x Value, t types.Type, source ast.Node) Value {
|
||||
a := &TypeAssert{X: x, AssertedType: t}
|
||||
a.setType(t)
|
||||
return f.emit(a, source)
|
||||
}
|
||||
|
||||
// emitTypeTest emits to f a type test value,ok := x.(t) and returns
|
||||
// a (value, ok) tuple. x.Type() must be an interface.
|
||||
func emitTypeTest(f *Function, x Value, t types.Type, source ast.Node) Value {
|
||||
a := &TypeAssert{
|
||||
X: x,
|
||||
AssertedType: t,
|
||||
CommaOk: true,
|
||||
}
|
||||
a.setType(types.NewTuple(
|
||||
newVar("value", t),
|
||||
varOk,
|
||||
))
|
||||
return f.emit(a, source)
|
||||
}
|
||||
|
||||
// emitTailCall emits to f a function call in tail position. The
|
||||
// caller is responsible for all fields of 'call' except its type.
|
||||
// Intended for wrapper methods.
|
||||
// Precondition: f does/will not use deferred procedure calls.
|
||||
// Postcondition: f.currentBlock is nil.
|
||||
func emitTailCall(f *Function, call *Call, source ast.Node) {
|
||||
tresults := f.Signature.Results()
|
||||
nr := tresults.Len()
|
||||
if nr == 1 {
|
||||
call.typ = tresults.At(0).Type()
|
||||
} else {
|
||||
call.typ = tresults
|
||||
}
|
||||
tuple := f.emit(call, source)
|
||||
var ret Return
|
||||
switch nr {
|
||||
case 0:
|
||||
// no-op
|
||||
case 1:
|
||||
ret.Results = []Value{tuple}
|
||||
default:
|
||||
for i := 0; i < nr; i++ {
|
||||
v := emitExtract(f, tuple, i, source)
|
||||
// TODO(adonovan): in principle, this is required:
|
||||
// v = emitConv(f, o.Type, f.Signature.Results[i].Type)
|
||||
// but in practice emitTailCall is only used when
|
||||
// the types exactly match.
|
||||
ret.Results = append(ret.Results, v)
|
||||
}
|
||||
}
|
||||
|
||||
f.Exit = f.newBasicBlock("exit")
|
||||
emitJump(f, f.Exit, source)
|
||||
f.currentBlock = f.Exit
|
||||
f.emit(&ret, source)
|
||||
f.currentBlock = nil
|
||||
}
|
||||
|
||||
// emitImplicitSelections emits to f code to apply the sequence of
|
||||
// implicit field selections specified by indices to base value v, and
|
||||
// returns the selected value.
|
||||
//
|
||||
// If v is the address of a struct, the result will be the address of
|
||||
// a field; if it is the value of a struct, the result will be the
|
||||
// value of a field.
|
||||
func emitImplicitSelections(f *Function, v Value, indices []int, source ast.Node) Value {
|
||||
for _, index := range indices {
|
||||
// We may have a generic type containing a pointer, or a pointer to a generic type containing a struct. A
|
||||
// pointer to a generic containing a pointer to a struct shouldn't be possible because the outer pointer gets
|
||||
// dereferenced implicitly before we get here.
|
||||
fld := typeutil.CoreType(deref(v.Type())).Underlying().(*types.Struct).Field(index)
|
||||
|
||||
if isPointer(v.Type()) {
|
||||
instr := &FieldAddr{
|
||||
X: v,
|
||||
Field: index,
|
||||
}
|
||||
instr.setType(types.NewPointer(fld.Type()))
|
||||
v = f.emit(instr, source)
|
||||
// Load the field's value iff indirectly embedded.
|
||||
if isPointer(fld.Type()) {
|
||||
v = emitLoad(f, v, source)
|
||||
}
|
||||
} else {
|
||||
instr := &Field{
|
||||
X: v,
|
||||
Field: index,
|
||||
}
|
||||
instr.setType(fld.Type())
|
||||
v = f.emit(instr, source)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// emitFieldSelection emits to f code to select the index'th field of v.
|
||||
//
|
||||
// If wantAddr, the input must be a pointer-to-struct and the result
|
||||
// will be the field's address; otherwise the result will be the
|
||||
// field's value.
|
||||
// Ident id is used for position and debug info.
|
||||
func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast.Ident) Value {
|
||||
// We may have a generic type containing a pointer, or a pointer to a generic type containing a struct. A
|
||||
// pointer to a generic containing a pointer to a struct shouldn't be possible because the outer pointer gets
|
||||
// dereferenced implicitly before we get here.
|
||||
vut := typeutil.CoreType(deref(v.Type())).Underlying().(*types.Struct)
|
||||
fld := vut.Field(index)
|
||||
if isPointer(v.Type()) {
|
||||
instr := &FieldAddr{
|
||||
X: v,
|
||||
Field: index,
|
||||
}
|
||||
instr.setSource(id)
|
||||
instr.setType(types.NewPointer(fld.Type()))
|
||||
v = f.emit(instr, id)
|
||||
// Load the field's value iff we don't want its address.
|
||||
if !wantAddr {
|
||||
v = emitLoad(f, v, id)
|
||||
}
|
||||
} else {
|
||||
instr := &Field{
|
||||
X: v,
|
||||
Field: index,
|
||||
}
|
||||
instr.setSource(id)
|
||||
instr.setType(fld.Type())
|
||||
v = f.emit(instr, id)
|
||||
}
|
||||
emitDebugRef(f, id, v, wantAddr)
|
||||
return v
|
||||
}
|
||||
|
||||
// zeroValue emits to f code to produce a zero value of type t,
|
||||
// and returns it.
|
||||
func zeroValue(f *Function, t types.Type, source ast.Node) Value {
|
||||
return emitConst(f, zeroConst(t))
|
||||
}
|
||||
|
||||
type constKey struct {
|
||||
typ types.Type
|
||||
value constant.Value
|
||||
}
|
||||
|
||||
func emitConst(f *Function, c Constant) Constant {
|
||||
if f.consts == nil {
|
||||
f.consts = map[constKey]constValue{}
|
||||
}
|
||||
|
||||
typ := c.Type()
|
||||
var val constant.Value
|
||||
switch c := c.(type) {
|
||||
case *Const:
|
||||
val = c.Value
|
||||
case *ArrayConst, *GenericConst:
|
||||
// These can only represent zero values, so all we need is the type
|
||||
case *AggregateConst:
|
||||
candidates, _ := f.aggregateConsts.At(c.typ)
|
||||
for _, candidate := range candidates {
|
||||
if c.equal(candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
for i := range c.Values {
|
||||
c.Values[i] = emitConst(f, c.Values[i].(Constant))
|
||||
}
|
||||
|
||||
c.setBlock(f.Blocks[0])
|
||||
rands := c.Operands(nil)
|
||||
updateOperandsReferrers(c, rands)
|
||||
candidates = append(candidates, c)
|
||||
f.aggregateConsts.Set(c.typ, candidates)
|
||||
return c
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected type %T", c))
|
||||
}
|
||||
k := constKey{
|
||||
typ: typ,
|
||||
value: val,
|
||||
}
|
||||
dup, ok := f.consts[k]
|
||||
if ok {
|
||||
return dup.c
|
||||
} else {
|
||||
c.setBlock(f.Blocks[0])
|
||||
f.consts[k] = constValue{
|
||||
c: c,
|
||||
idx: len(f.consts),
|
||||
}
|
||||
rands := c.Operands(nil)
|
||||
updateOperandsReferrers(c, rands)
|
||||
return c
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
const hello = `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
const message = "Hello, World!"
|
||||
|
||||
func main() {
|
||||
fmt.Println(message)
|
||||
}
|
||||
`
|
||||
|
||||
// This program demonstrates how to run the IR builder on a single
|
||||
// package of one or more already-parsed files. Its dependencies are
|
||||
// loaded from compiler export data. This is what you'd typically use
|
||||
// for a compiler; it does not depend on golang.org/x/tools/go/loader.
|
||||
//
|
||||
// It shows the printed representation of packages, functions, and
|
||||
// instructions. Within the function listing, the name of each
|
||||
// BasicBlock such as ".0.entry" is printed left-aligned, followed by
|
||||
// the block's Instructions.
|
||||
//
|
||||
// For each instruction that defines an IR virtual register
|
||||
// (i.e. implements Value), the type of that value is shown in the
|
||||
// right column.
|
||||
//
|
||||
// Build and run the irdump.go program if you want a standalone tool
|
||||
// with similar functionality. It is located at
|
||||
// honnef.co/go/tools/internal/cmd/irdump.
|
||||
func Example_buildPackage() {
|
||||
// Parse the source files.
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "hello.go", hello, parser.ParseComments|parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
fmt.Print(err) // parse error
|
||||
return
|
||||
}
|
||||
files := []*ast.File{f}
|
||||
|
||||
// Create the type-checker's package.
|
||||
pkg := types.NewPackage("hello", "")
|
||||
|
||||
// Type-check the package, load dependencies.
|
||||
// Create and build the IR program.
|
||||
hello, _, err := irutil.BuildPackage(
|
||||
&types.Config{Importer: importer.Default()}, fset, pkg, files, ir.SanityCheckFunctions)
|
||||
if err != nil {
|
||||
fmt.Print(err) // type error in some package
|
||||
return
|
||||
}
|
||||
|
||||
// Print out the package.
|
||||
hello.WriteTo(os.Stdout)
|
||||
|
||||
// Print out the package-level functions.
|
||||
// Replace interface{} with any so the tests work for Go 1.17 and Go 1.18.
|
||||
{
|
||||
var buf bytes.Buffer
|
||||
ir.WriteFunction(&buf, hello.Func("init"))
|
||||
fmt.Print(strings.ReplaceAll(buf.String(), "interface{}", "any"))
|
||||
}
|
||||
{
|
||||
var buf bytes.Buffer
|
||||
ir.WriteFunction(&buf, hello.Func("main"))
|
||||
fmt.Print(strings.ReplaceAll(buf.String(), "interface{}", "any"))
|
||||
}
|
||||
|
||||
// Output:
|
||||
// package hello:
|
||||
// func init func()
|
||||
// var init$guard bool
|
||||
// func main func()
|
||||
// const message message = Const <untyped string> {"Hello, World!"}
|
||||
//
|
||||
// # Name: hello.init
|
||||
// # Package: hello
|
||||
// # Synthetic: package initializer
|
||||
// func init():
|
||||
// b0: # entry
|
||||
// t1 = Const <bool> {true}
|
||||
// t2 = Load <bool> init$guard
|
||||
// If t2 → b1 b2
|
||||
//
|
||||
// b1: ← b0 b2 # exit
|
||||
// Return
|
||||
//
|
||||
// b2: ← b0 # init.start
|
||||
// Store {bool} init$guard t1
|
||||
// t6 = Call <()> fmt.init
|
||||
// Jump → b1
|
||||
//
|
||||
// # Name: hello.main
|
||||
// # Package: hello
|
||||
// # Location: hello.go:8:1
|
||||
// func main():
|
||||
// b0: # entry
|
||||
// t1 = Const <string> {"Hello, World!"}
|
||||
// t2 = Const <int> {0}
|
||||
// t3 = HeapAlloc <*[1]any>
|
||||
// t4 = IndexAddr <*any> t3 t2
|
||||
// t5 = MakeInterface <any> t1
|
||||
// Store {any} t4 t5
|
||||
// t7 = Slice <[]any> t3 <nil> <nil> <nil>
|
||||
// t8 = Call <(n int, err error)> fmt.Println t7
|
||||
// Jump → b1
|
||||
//
|
||||
// b1: ← b0 # exit
|
||||
// Return
|
||||
}
|
||||
|
||||
// This example builds IR code for a set of packages using the
|
||||
// x/tools/go/packages API. This is what you would typically use for a
|
||||
// analysis capable of operating on a single package.
|
||||
func Example_loadPackages() {
|
||||
// Load, parse, and type-check the initial packages.
|
||||
cfg := &packages.Config{Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
initial, err := packages.Load(cfg, "fmt", "net/http")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Stop if any package had errors.
|
||||
// This step is optional; without it, the next step
|
||||
// will create IR for only a subset of packages.
|
||||
if packages.PrintErrors(initial) > 0 {
|
||||
log.Fatalf("packages contain errors")
|
||||
}
|
||||
|
||||
// Create IR packages for all well-typed packages.
|
||||
prog, pkgs := irutil.Packages(initial, ir.PrintPackages, nil)
|
||||
_ = prog
|
||||
|
||||
// Build IR code for the well-typed initial packages.
|
||||
for _, p := range pkgs {
|
||||
if p != nil {
|
||||
p.Build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This example builds IR code for a set of packages plus all their dependencies,
|
||||
// using the x/tools/go/packages API.
|
||||
// This is what you'd typically use for a whole-program analysis.
|
||||
func Example_loadWholeProgram() {
|
||||
// Load, parse, and type-check the whole program.
|
||||
cfg := packages.Config{Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps}
|
||||
initial, err := packages.Load(&cfg, "fmt", "net/http")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create IR packages for well-typed packages and their dependencies.
|
||||
prog, pkgs := irutil.AllPackages(initial, ir.PrintPackages, nil)
|
||||
_ = pkgs
|
||||
|
||||
// Build IR code for the whole program.
|
||||
prog.Build()
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package ir
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
)
|
||||
|
||||
func (b *builder) buildExits(fn *Function) {
|
||||
if obj := fn.Object(); obj != nil {
|
||||
switch obj.Pkg().Path() {
|
||||
case "runtime":
|
||||
switch obj.Name() {
|
||||
case "exit":
|
||||
fn.NoReturn = AlwaysExits
|
||||
return
|
||||
case "throw":
|
||||
fn.NoReturn = AlwaysExits
|
||||
return
|
||||
case "Goexit":
|
||||
fn.NoReturn = AlwaysUnwinds
|
||||
return
|
||||
}
|
||||
case "go.uber.org/zap":
|
||||
switch obj.(*types.Func).FullName() {
|
||||
case "(*go.uber.org/zap.Logger).Fatal",
|
||||
"(*go.uber.org/zap.SugaredLogger).Fatal",
|
||||
"(*go.uber.org/zap.SugaredLogger).Fatalw",
|
||||
"(*go.uber.org/zap.SugaredLogger).Fatalf":
|
||||
// Technically, this method does not unconditionally exit
|
||||
// the process. It dynamically calls a function stored in
|
||||
// the logger. If the function is nil, it defaults to
|
||||
// os.Exit.
|
||||
//
|
||||
// The main intent of this method is to terminate the
|
||||
// process, and that's what the vast majority of people
|
||||
// will use it for. We'll happily accept some false
|
||||
// negatives to avoid a lot of false positives.
|
||||
fn.NoReturn = AlwaysExits
|
||||
case "(*go.uber.org/zap.Logger).Panic",
|
||||
"(*go.uber.org/zap.SugaredLogger).Panicw",
|
||||
"(*go.uber.org/zap.SugaredLogger).Panicf":
|
||||
fn.NoReturn = AlwaysUnwinds
|
||||
return
|
||||
case "(*go.uber.org/zap.Logger).DPanic",
|
||||
"(*go.uber.org/zap.SugaredLogger).DPanicf",
|
||||
"(*go.uber.org/zap.SugaredLogger).DPanicw":
|
||||
// These methods will only panic in development.
|
||||
}
|
||||
case "github.com/sirupsen/logrus":
|
||||
switch obj.(*types.Func).FullName() {
|
||||
case "(*github.com/sirupsen/logrus.Logger).Exit":
|
||||
// Technically, this method does not unconditionally exit
|
||||
// the process. It dynamically calls a function stored in
|
||||
// the logger. If the function is nil, it defaults to
|
||||
// os.Exit.
|
||||
//
|
||||
// The main intent of this method is to terminate the
|
||||
// process, and that's what the vast majority of people
|
||||
// will use it for. We'll happily accept some false
|
||||
// negatives to avoid a lot of false positives.
|
||||
fn.NoReturn = AlwaysExits
|
||||
return
|
||||
case "(*github.com/sirupsen/logrus.Logger).Panic",
|
||||
"(*github.com/sirupsen/logrus.Logger).Panicf",
|
||||
"(*github.com/sirupsen/logrus.Logger).Panicln":
|
||||
|
||||
// These methods will always panic, but that's not
|
||||
// statically known from the code alone, because they
|
||||
// take a detour through the generic Log methods.
|
||||
fn.NoReturn = AlwaysUnwinds
|
||||
return
|
||||
case "(*github.com/sirupsen/logrus.Entry).Panicf",
|
||||
"(*github.com/sirupsen/logrus.Entry).Panicln":
|
||||
|
||||
// Entry.Panic has an explicit panic, but Panicf and
|
||||
// Panicln do not, relying fully on the generic Log
|
||||
// method.
|
||||
fn.NoReturn = AlwaysUnwinds
|
||||
return
|
||||
case "(*github.com/sirupsen/logrus.Logger).Log",
|
||||
"(*github.com/sirupsen/logrus.Logger).Logf",
|
||||
"(*github.com/sirupsen/logrus.Logger).Logln":
|
||||
// TODO(dh): we cannot handle these cases. Whether they
|
||||
// exit or unwind depends on the level, which is set
|
||||
// via the first argument. We don't currently support
|
||||
// call-site-specific exit information.
|
||||
}
|
||||
case "github.com/golang/glog":
|
||||
switch obj.(*types.Func).FullName() {
|
||||
case "github.com/golang/glog.Exit",
|
||||
"github.com/golang/glog.ExitDepth",
|
||||
"github.com/golang/glog.Exitf",
|
||||
"github.com/golang/glog.Exitln",
|
||||
"github.com/golang/glog.Fatal",
|
||||
"github.com/golang/glog.FatalDepth",
|
||||
"github.com/golang/glog.Fatalf",
|
||||
"github.com/golang/glog.Fatalln":
|
||||
// all of these call os.Exit after logging
|
||||
fn.NoReturn = AlwaysExits
|
||||
}
|
||||
case "k8s.io/klog":
|
||||
switch obj.(*types.Func).FullName() {
|
||||
case "k8s.io/klog.Exit",
|
||||
"k8s.io/klog.ExitDepth",
|
||||
"k8s.io/klog.Exitf",
|
||||
"k8s.io/klog.Exitln",
|
||||
"k8s.io/klog.Fatal",
|
||||
"k8s.io/klog.FatalDepth",
|
||||
"k8s.io/klog.Fatalf",
|
||||
"k8s.io/klog.Fatalln":
|
||||
// all of these call os.Exit after logging
|
||||
fn.NoReturn = AlwaysExits
|
||||
}
|
||||
case "k8s.io/klog/v2":
|
||||
switch obj.(*types.Func).FullName() {
|
||||
case "k8s.io/klog/v2.Exit",
|
||||
"k8s.io/klog/v2.ExitDepth",
|
||||
"k8s.io/klog/v2.Exitf",
|
||||
"k8s.io/klog/v2.Exitln",
|
||||
"k8s.io/klog/v2.Fatal",
|
||||
"k8s.io/klog/v2.FatalDepth",
|
||||
"k8s.io/klog/v2.Fatalf",
|
||||
"k8s.io/klog/v2.Fatalln":
|
||||
// all of these call os.Exit after logging
|
||||
fn.NoReturn = AlwaysExits
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isRecoverCall := func(instr Instruction) bool {
|
||||
if instr, ok := instr.(*Call); ok {
|
||||
if builtin, ok := instr.Call.Value.(*Builtin); ok {
|
||||
if builtin.Name() == "recover" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
both := NewBlockSet(len(fn.Blocks))
|
||||
exits := NewBlockSet(len(fn.Blocks))
|
||||
unwinds := NewBlockSet(len(fn.Blocks))
|
||||
recovers := false
|
||||
for _, u := range fn.Blocks {
|
||||
for _, instr := range u.Instrs {
|
||||
instrSwitch:
|
||||
switch instr := instr.(type) {
|
||||
case *Defer:
|
||||
if recovers {
|
||||
// avoid doing extra work, we already know that this function calls recover
|
||||
continue
|
||||
}
|
||||
call := instr.Call.StaticCallee()
|
||||
if call == nil {
|
||||
// not a static call, so we can't be sure the
|
||||
// deferred call isn't calling recover
|
||||
recovers = true
|
||||
break
|
||||
}
|
||||
if call.Package() == fn.Package() {
|
||||
b.buildFunction(call)
|
||||
}
|
||||
if len(call.Blocks) == 0 {
|
||||
// external function, we don't know what's
|
||||
// happening inside it
|
||||
//
|
||||
// TODO(dh): this includes functions from
|
||||
// imported packages, due to how go/analysis
|
||||
// works. We could introduce another fact,
|
||||
// like we've done for exiting and unwinding.
|
||||
recovers = true
|
||||
break
|
||||
}
|
||||
for _, y := range call.Blocks {
|
||||
for _, instr2 := range y.Instrs {
|
||||
if isRecoverCall(instr2) {
|
||||
recovers = true
|
||||
break instrSwitch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *Panic:
|
||||
both.Add(u)
|
||||
unwinds.Add(u)
|
||||
|
||||
case CallInstruction:
|
||||
switch instr.(type) {
|
||||
case *Defer, *Call:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if instr.Common().IsInvoke() {
|
||||
// give up
|
||||
return
|
||||
}
|
||||
var call *Function
|
||||
switch instr.Common().Value.(type) {
|
||||
case *Function, *MakeClosure:
|
||||
call = instr.Common().StaticCallee()
|
||||
case *Builtin:
|
||||
// the only builtins that affect control flow are
|
||||
// panic and recover, and we've already handled
|
||||
// those
|
||||
continue
|
||||
default:
|
||||
// dynamic dispatch
|
||||
return
|
||||
}
|
||||
// buildFunction is idempotent. if we're part of a
|
||||
// (mutually) recursive call chain, then buildFunction
|
||||
// will immediately return, and fn.WillExit will be false.
|
||||
if call.Package() == fn.Package() {
|
||||
b.buildFunction(call)
|
||||
}
|
||||
switch call.NoReturn {
|
||||
case AlwaysExits:
|
||||
both.Add(u)
|
||||
exits.Add(u)
|
||||
case AlwaysUnwinds:
|
||||
both.Add(u)
|
||||
unwinds.Add(u)
|
||||
case NeverReturns:
|
||||
both.Add(u)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// depth-first search trying to find a path to the exit block that
|
||||
// doesn't cross any of the blacklisted blocks
|
||||
seen := NewBlockSet(len(fn.Blocks))
|
||||
var findPath func(root *BasicBlock, bl *BlockSet) bool
|
||||
findPath = func(root *BasicBlock, bl *BlockSet) bool {
|
||||
if root == fn.Exit {
|
||||
return true
|
||||
}
|
||||
if seen.Has(root) {
|
||||
return false
|
||||
}
|
||||
if bl.Has(root) {
|
||||
return false
|
||||
}
|
||||
seen.Add(root)
|
||||
for _, succ := range root.Succs {
|
||||
if findPath(succ, bl) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
findPathEntry := func(root *BasicBlock, bl *BlockSet) bool {
|
||||
if bl.Num() == 0 {
|
||||
return true
|
||||
}
|
||||
seen.Clear()
|
||||
return findPath(root, bl)
|
||||
}
|
||||
|
||||
if !findPathEntry(fn.Blocks[0], exits) {
|
||||
fn.NoReturn = AlwaysExits
|
||||
} else if !recovers {
|
||||
// Only consider unwinding and "never returns" if we don't
|
||||
// call recover. If we do call recover, then panics don't
|
||||
// bubble up the stack.
|
||||
|
||||
// TODO(dh): the position of the defer matters. If we
|
||||
// unconditionally terminate before we defer a recover, then
|
||||
// the recover is ineffective.
|
||||
|
||||
if !findPathEntry(fn.Blocks[0], unwinds) {
|
||||
fn.NoReturn = AlwaysUnwinds
|
||||
} else if !findPathEntry(fn.Blocks[0], both) {
|
||||
fn.NoReturn = NeverReturns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) addUnreachables(fn *Function) {
|
||||
var unreachable *BasicBlock
|
||||
|
||||
for _, bb := range fn.Blocks {
|
||||
instrLoop:
|
||||
for i, instr := range bb.Instrs {
|
||||
if instr, ok := instr.(*Call); ok {
|
||||
var call *Function
|
||||
switch v := instr.Common().Value.(type) {
|
||||
case *Function:
|
||||
call = v
|
||||
case *MakeClosure:
|
||||
call = v.Fn.(*Function)
|
||||
}
|
||||
if call == nil {
|
||||
continue
|
||||
}
|
||||
if call.Package() == fn.Package() {
|
||||
// make sure we have information on all functions in this package
|
||||
b.buildFunction(call)
|
||||
}
|
||||
switch call.NoReturn {
|
||||
case AlwaysExits:
|
||||
// This call will cause the process to terminate.
|
||||
// Remove remaining instructions in the block and
|
||||
// replace any control flow with Unreachable.
|
||||
for _, succ := range bb.Succs {
|
||||
succ.removePred(bb)
|
||||
}
|
||||
bb.Succs = bb.Succs[:0]
|
||||
|
||||
bb.Instrs = bb.Instrs[:i+1]
|
||||
bb.emit(new(Unreachable), instr.Source())
|
||||
addEdge(bb, fn.Exit)
|
||||
break instrLoop
|
||||
|
||||
case AlwaysUnwinds:
|
||||
// This call will cause the goroutine to terminate
|
||||
// and defers to run (i.e. a panic or
|
||||
// runtime.Goexit). Remove remaining instructions
|
||||
// in the block and replace any control flow with
|
||||
// an unconditional jump to the exit block.
|
||||
for _, succ := range bb.Succs {
|
||||
succ.removePred(bb)
|
||||
}
|
||||
bb.Succs = bb.Succs[:0]
|
||||
|
||||
bb.Instrs = bb.Instrs[:i+1]
|
||||
bb.emit(new(Jump), instr.Source())
|
||||
addEdge(bb, fn.Exit)
|
||||
break instrLoop
|
||||
|
||||
case NeverReturns:
|
||||
// This call will either cause the goroutine to
|
||||
// terminate, or the process to terminate. Remove
|
||||
// remaining instructions in the block and replace
|
||||
// any control flow with a conditional jump to
|
||||
// either the exit block, or Unreachable.
|
||||
for _, succ := range bb.Succs {
|
||||
succ.removePred(bb)
|
||||
}
|
||||
bb.Succs = bb.Succs[:0]
|
||||
|
||||
bb.Instrs = bb.Instrs[:i+1]
|
||||
var c Call
|
||||
c.Call.Value = &Builtin{
|
||||
name: "ir:noreturnWasPanic",
|
||||
sig: types.NewSignatureType(nil, nil, nil,
|
||||
types.NewTuple(),
|
||||
types.NewTuple(anonVar(types.Typ[types.Bool])),
|
||||
false,
|
||||
),
|
||||
}
|
||||
c.setType(types.Typ[types.Bool])
|
||||
|
||||
if unreachable == nil {
|
||||
unreachable = fn.newBasicBlock("unreachable")
|
||||
unreachable.emit(&Unreachable{}, nil)
|
||||
addEdge(unreachable, fn.Exit)
|
||||
}
|
||||
|
||||
bb.emit(&c, instr.Source())
|
||||
bb.emit(&If{Cond: &c}, instr.Source())
|
||||
addEdge(bb, fn.Exit)
|
||||
addEdge(bb, unreachable)
|
||||
break instrLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,957 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file implements the Function and BasicBlock types.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// addEdge adds a control-flow graph edge from from to to.
|
||||
func addEdge(from, to *BasicBlock) {
|
||||
from.Succs = append(from.Succs, to)
|
||||
to.Preds = append(to.Preds, from)
|
||||
}
|
||||
|
||||
// Control returns the last instruction in the block.
|
||||
func (b *BasicBlock) Control() Instruction {
|
||||
if len(b.Instrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return b.Instrs[len(b.Instrs)-1]
|
||||
}
|
||||
|
||||
// SigmaFor returns the sigma node for v coming from pred.
|
||||
func (b *BasicBlock) SigmaFor(v Value, pred *BasicBlock) *Sigma {
|
||||
for _, instr := range b.Instrs {
|
||||
sigma, ok := instr.(*Sigma)
|
||||
if !ok {
|
||||
// no more sigmas
|
||||
return nil
|
||||
}
|
||||
if sigma.From == pred && sigma.X == v {
|
||||
return sigma
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parent returns the function that contains block b.
|
||||
func (b *BasicBlock) Parent() *Function { return b.parent }
|
||||
|
||||
// String returns a human-readable label of this block.
|
||||
// It is not guaranteed unique within the function.
|
||||
func (b *BasicBlock) String() string {
|
||||
return fmt.Sprintf("%d", b.Index)
|
||||
}
|
||||
|
||||
// emit appends an instruction to the current basic block.
|
||||
// If the instruction defines a Value, it is returned.
|
||||
func (b *BasicBlock) emit(i Instruction, source ast.Node) Value {
|
||||
i.setSource(source)
|
||||
i.setBlock(b)
|
||||
b.Instrs = append(b.Instrs, i)
|
||||
v, _ := i.(Value)
|
||||
return v
|
||||
}
|
||||
|
||||
// predIndex returns the i such that b.Preds[i] == c or panics if
|
||||
// there is none.
|
||||
func (b *BasicBlock) predIndex(c *BasicBlock) int {
|
||||
for i, pred := range b.Preds {
|
||||
if pred == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf("no edge %s -> %s", c, b))
|
||||
}
|
||||
|
||||
// succIndex returns the i such that b.Succs[i] == c or -1 if there is none.
|
||||
func (b *BasicBlock) succIndex(c *BasicBlock) int {
|
||||
for i, succ := range b.Succs {
|
||||
if succ == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// hasPhi returns true if b.Instrs contains φ-nodes.
|
||||
func (b *BasicBlock) hasPhi() bool {
|
||||
_, ok := b.Instrs[0].(*Phi)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (b *BasicBlock) Phis() []Instruction {
|
||||
return b.phis()
|
||||
}
|
||||
|
||||
// phis returns the prefix of b.Instrs containing all the block's φ-nodes.
|
||||
func (b *BasicBlock) phis() []Instruction {
|
||||
for i, instr := range b.Instrs {
|
||||
if _, ok := instr.(*Phi); !ok {
|
||||
return b.Instrs[:i]
|
||||
}
|
||||
}
|
||||
return nil // unreachable in well-formed blocks
|
||||
}
|
||||
|
||||
// replacePred replaces all occurrences of p in b's predecessor list with q.
|
||||
// Ordinarily there should be at most one.
|
||||
func (b *BasicBlock) replacePred(p, q *BasicBlock) {
|
||||
for i, pred := range b.Preds {
|
||||
if pred == p {
|
||||
b.Preds[i] = q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replaceSucc replaces all occurrences of p in b's successor list with q.
|
||||
// Ordinarily there should be at most one.
|
||||
func (b *BasicBlock) replaceSucc(p, q *BasicBlock) {
|
||||
for i, succ := range b.Succs {
|
||||
if succ == p {
|
||||
b.Succs[i] = q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removePred removes all occurrences of p in b's
|
||||
// predecessor list and φ-nodes.
|
||||
// Ordinarily there should be at most one.
|
||||
func (b *BasicBlock) removePred(p *BasicBlock) {
|
||||
phis := b.phis()
|
||||
|
||||
// We must preserve edge order for φ-nodes.
|
||||
j := 0
|
||||
for i, pred := range b.Preds {
|
||||
if pred != p {
|
||||
b.Preds[j] = b.Preds[i]
|
||||
// Strike out φ-edge too.
|
||||
for _, instr := range phis {
|
||||
phi := instr.(*Phi)
|
||||
phi.Edges[j] = phi.Edges[i]
|
||||
}
|
||||
j++
|
||||
}
|
||||
}
|
||||
// Nil out b.Preds[j:] and φ-edges[j:] to aid GC.
|
||||
for i := j; i < len(b.Preds); i++ {
|
||||
b.Preds[i] = nil
|
||||
for _, instr := range phis {
|
||||
instr.(*Phi).Edges[i] = nil
|
||||
}
|
||||
}
|
||||
b.Preds = b.Preds[:j]
|
||||
for _, instr := range phis {
|
||||
phi := instr.(*Phi)
|
||||
phi.Edges = phi.Edges[:j]
|
||||
}
|
||||
}
|
||||
|
||||
// Destinations associated with unlabelled for/switch/select stmts.
|
||||
// We push/pop one of these as we enter/leave each construct and for
|
||||
// each BranchStmt we scan for the innermost target of the right type.
|
||||
type targets struct {
|
||||
tail *targets // rest of stack
|
||||
_break *BasicBlock
|
||||
_continue *BasicBlock
|
||||
_fallthrough *BasicBlock
|
||||
}
|
||||
|
||||
// Destinations associated with a labelled block.
|
||||
// We populate these as labels are encountered in forward gotos or
|
||||
// labelled statements.
|
||||
type lblock struct {
|
||||
_goto *BasicBlock
|
||||
_break *BasicBlock
|
||||
_continue *BasicBlock
|
||||
}
|
||||
|
||||
// labelledBlock returns the branch target associated with the
|
||||
// specified label, creating it if needed.
|
||||
func (f *Function) labelledBlock(label *ast.Ident) *lblock {
|
||||
obj := f.Pkg.info.ObjectOf(label)
|
||||
if obj == nil {
|
||||
// Blank label, as in '_:' - don't store to f.lblocks, this label can never be referred to; just return a fresh
|
||||
// lbock.
|
||||
return &lblock{_goto: f.newBasicBlock(label.Name)}
|
||||
}
|
||||
|
||||
lb := f.lblocks[obj]
|
||||
if lb == nil {
|
||||
lb = &lblock{_goto: f.newBasicBlock(label.Name)}
|
||||
if f.lblocks == nil {
|
||||
f.lblocks = make(map[types.Object]*lblock)
|
||||
}
|
||||
f.lblocks[obj] = lb
|
||||
}
|
||||
return lb
|
||||
}
|
||||
|
||||
// addParam adds a (non-escaping) parameter to f.Params of the
|
||||
// specified name, type and source position.
|
||||
func (f *Function) addParam(name string, typ types.Type, source ast.Node) *Parameter {
|
||||
var b *BasicBlock
|
||||
if len(f.Blocks) > 0 {
|
||||
b = f.Blocks[0]
|
||||
}
|
||||
v := &Parameter{
|
||||
name: name,
|
||||
}
|
||||
v.setBlock(b)
|
||||
v.setType(typ)
|
||||
v.setSource(source)
|
||||
f.Params = append(f.Params, v)
|
||||
if b != nil {
|
||||
// There may be no blocks if this function has no body. We
|
||||
// still create params, but aren't interested in the
|
||||
// instruction.
|
||||
f.Blocks[0].Instrs = append(f.Blocks[0].Instrs, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (f *Function) addParamObj(obj types.Object, source ast.Node) *Parameter {
|
||||
name := obj.Name()
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("arg%d", len(f.Params))
|
||||
}
|
||||
param := f.addParam(name, obj.Type(), source)
|
||||
param.object = obj
|
||||
return param
|
||||
}
|
||||
|
||||
// addSpilledParam declares a parameter that is pre-spilled to the
|
||||
// stack; the function body will load/store the spilled location.
|
||||
// Subsequent lifting will eliminate spills where possible.
|
||||
func (f *Function) addSpilledParam(obj types.Object, source ast.Node) {
|
||||
param := f.addParamObj(obj, source)
|
||||
spill := &Alloc{}
|
||||
spill.setType(types.NewPointer(obj.Type()))
|
||||
spill.source = source
|
||||
f.objects[obj] = spill
|
||||
f.Locals = append(f.Locals, spill)
|
||||
f.emit(spill, source)
|
||||
emitStore(f, spill, param, source)
|
||||
// f.emit(&Store{Addr: spill, Val: param})
|
||||
}
|
||||
|
||||
// startBody initializes the function prior to generating IR code for its body.
|
||||
// Precondition: f.Type() already set.
|
||||
func (f *Function) startBody() {
|
||||
entry := f.newBasicBlock("entry")
|
||||
f.currentBlock = entry
|
||||
f.objects = make(map[types.Object]Value) // needed for some synthetics, e.g. init
|
||||
}
|
||||
|
||||
func (f *Function) blockset(i int) *BlockSet {
|
||||
bs := &f.blocksets[i]
|
||||
if len(bs.values) != len(f.Blocks) {
|
||||
if cap(bs.values) >= len(f.Blocks) {
|
||||
bs.values = bs.values[:len(f.Blocks)]
|
||||
bs.Clear()
|
||||
} else {
|
||||
bs.values = make([]bool, len(f.Blocks))
|
||||
}
|
||||
} else {
|
||||
bs.Clear()
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (f *Function) exitBlock() {
|
||||
old := f.currentBlock
|
||||
|
||||
f.Exit = f.newBasicBlock("exit")
|
||||
f.currentBlock = f.Exit
|
||||
|
||||
ret := f.results()
|
||||
results := make([]Value, len(ret))
|
||||
// Run function calls deferred in this
|
||||
// function when explicitly returning from it.
|
||||
f.emit(new(RunDefers), nil)
|
||||
for i, r := range ret {
|
||||
results[i] = emitLoad(f, r, nil)
|
||||
}
|
||||
|
||||
f.emit(&Return{Results: results}, nil)
|
||||
f.currentBlock = old
|
||||
}
|
||||
|
||||
// createSyntacticParams populates f.Params and generates code (spills
|
||||
// and named result locals) for all the parameters declared in the
|
||||
// syntax. In addition it populates the f.objects mapping.
|
||||
//
|
||||
// Preconditions:
|
||||
// f.startBody() was called.
|
||||
// Postcondition:
|
||||
// len(f.Params) == len(f.Signature.Params) + (f.Signature.Recv() ? 1 : 0)
|
||||
func (f *Function) createSyntacticParams(recv *ast.FieldList, functype *ast.FuncType) {
|
||||
// Receiver (at most one inner iteration).
|
||||
if recv != nil {
|
||||
for _, field := range recv.List {
|
||||
for _, n := range field.Names {
|
||||
f.addSpilledParam(f.Pkg.info.Defs[n], n)
|
||||
}
|
||||
// Anonymous receiver? No need to spill.
|
||||
if field.Names == nil {
|
||||
f.addParamObj(f.Signature.Recv(), field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters.
|
||||
if functype.Params != nil {
|
||||
n := len(f.Params) // 1 if has recv, 0 otherwise
|
||||
for _, field := range functype.Params.List {
|
||||
for _, n := range field.Names {
|
||||
f.addSpilledParam(f.Pkg.info.Defs[n], n)
|
||||
}
|
||||
// Anonymous parameter? No need to spill.
|
||||
if field.Names == nil {
|
||||
f.addParamObj(f.Signature.Params().At(len(f.Params)-n), field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Named results.
|
||||
if functype.Results != nil {
|
||||
for _, field := range functype.Results.List {
|
||||
// Implicit "var" decl of locals for named results.
|
||||
for _, n := range field.Names {
|
||||
f.namedResults = append(f.namedResults, f.addLocalForIdent(n))
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.namedResults) == 0 {
|
||||
sig := f.Signature.Results()
|
||||
for i := 0; i < sig.Len(); i++ {
|
||||
// XXX position information
|
||||
v := f.addLocal(sig.At(i).Type(), nil)
|
||||
f.implicitResults = append(f.implicitResults, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func numberNodes(f *Function) {
|
||||
var base ID
|
||||
for _, b := range f.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
if instr == nil {
|
||||
continue
|
||||
}
|
||||
base++
|
||||
instr.setID(base)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateOperandsReferrers(instr Instruction, ops []*Value) {
|
||||
for _, op := range ops {
|
||||
if r := *op; r != nil {
|
||||
if refs := (*op).Referrers(); refs != nil {
|
||||
if len(*refs) == 0 {
|
||||
// per median, each value has two referrers, so we can avoid one call into growslice
|
||||
//
|
||||
// Note: we experimented with allocating
|
||||
// sequential scratch space, but we
|
||||
// couldn't find a value that gave better
|
||||
// performance than making many individual
|
||||
// allocations
|
||||
*refs = make([]Instruction, 1, 2)
|
||||
(*refs)[0] = instr
|
||||
} else {
|
||||
*refs = append(*refs, instr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildReferrers populates the def/use information in all non-nil
|
||||
// Value.Referrers slice.
|
||||
// Precondition: all such slices are initially empty.
|
||||
func buildReferrers(f *Function) {
|
||||
var rands []*Value
|
||||
|
||||
for _, b := range f.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
rands = instr.Operands(rands[:0]) // recycle storage
|
||||
updateOperandsReferrers(instr, rands)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range f.consts {
|
||||
rands = c.c.Operands(rands[:0])
|
||||
updateOperandsReferrers(c.c, rands)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Function) emitConsts() {
|
||||
defer func() {
|
||||
f.consts = nil
|
||||
f.aggregateConsts = typeutil.Map[[]*AggregateConst]{}
|
||||
}()
|
||||
|
||||
if len(f.Blocks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(dh): our deduplication only works on booleans and
|
||||
// integers. other constants are represented as pointers to
|
||||
// things.
|
||||
head := make([]constValue, 0, len(f.consts))
|
||||
for _, c := range f.consts {
|
||||
if len(*c.c.Referrers()) == 0 {
|
||||
// TODO(dh): killing a const may make other consts dead, too
|
||||
killInstruction(c.c)
|
||||
} else {
|
||||
head = append(head, c)
|
||||
}
|
||||
}
|
||||
sort.Slice(head, func(i, j int) bool {
|
||||
return head[i].idx < head[j].idx
|
||||
})
|
||||
entry := f.Blocks[0]
|
||||
instrs := make([]Instruction, 0, len(entry.Instrs)+len(head))
|
||||
for _, c := range head {
|
||||
instrs = append(instrs, c.c)
|
||||
}
|
||||
f.aggregateConsts.Iterate(func(key types.Type, value []*AggregateConst) {
|
||||
for _, c := range value {
|
||||
instrs = append(instrs, c)
|
||||
}
|
||||
})
|
||||
|
||||
instrs = append(instrs, entry.Instrs...)
|
||||
entry.Instrs = instrs
|
||||
}
|
||||
|
||||
// buildFakeExits ensures that every block in the function is
|
||||
// reachable in reverse from the Exit block. This is required to build
|
||||
// a full post-dominator tree, and to ensure the exit block's
|
||||
// inclusion in the dominator tree.
|
||||
func buildFakeExits(fn *Function) {
|
||||
// Find back-edges via forward DFS
|
||||
fn.fakeExits = BlockSet{values: make([]bool, len(fn.Blocks))}
|
||||
seen := fn.blockset(0)
|
||||
backEdges := fn.blockset(1)
|
||||
|
||||
var dfs func(b *BasicBlock)
|
||||
dfs = func(b *BasicBlock) {
|
||||
if !seen.Add(b) {
|
||||
backEdges.Add(b)
|
||||
return
|
||||
}
|
||||
for _, pred := range b.Succs {
|
||||
dfs(pred)
|
||||
}
|
||||
}
|
||||
dfs(fn.Blocks[0])
|
||||
buildLoop:
|
||||
for {
|
||||
seen := fn.blockset(2)
|
||||
var dfs func(b *BasicBlock)
|
||||
dfs = func(b *BasicBlock) {
|
||||
if !seen.Add(b) {
|
||||
return
|
||||
}
|
||||
for _, pred := range b.Preds {
|
||||
dfs(pred)
|
||||
}
|
||||
if b == fn.Exit {
|
||||
for _, b := range fn.Blocks {
|
||||
if fn.fakeExits.Has(b) {
|
||||
dfs(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dfs(fn.Exit)
|
||||
|
||||
for _, b := range fn.Blocks {
|
||||
if !seen.Has(b) && backEdges.Has(b) {
|
||||
// Block b is not reachable from the exit block. Add a
|
||||
// fake jump from b to exit, then try again. Note that we
|
||||
// only add one fake edge at a time, as it may make
|
||||
// multiple blocks reachable.
|
||||
//
|
||||
// We only consider those blocks that have back edges.
|
||||
// Any unreachable block that doesn't have a back edge
|
||||
// must flow into a loop, which by definition has a
|
||||
// back edge. Thus, by looking for loops, we should
|
||||
// need fewer fake edges overall.
|
||||
fn.fakeExits.Add(b)
|
||||
continue buildLoop
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// finishBody() finalizes the function after IR code generation of its body.
|
||||
func (f *Function) finishBody() {
|
||||
f.objects = nil
|
||||
f.currentBlock = nil
|
||||
f.lblocks = nil
|
||||
|
||||
// Remove from f.Locals any Allocs that escape to the heap.
|
||||
j := 0
|
||||
for _, l := range f.Locals {
|
||||
if !l.Heap {
|
||||
f.Locals[j] = l
|
||||
j++
|
||||
}
|
||||
}
|
||||
// Nil out f.Locals[j:] to aid GC.
|
||||
for i := j; i < len(f.Locals); i++ {
|
||||
f.Locals[i] = nil
|
||||
}
|
||||
f.Locals = f.Locals[:j]
|
||||
|
||||
optimizeBlocks(f)
|
||||
buildFakeExits(f)
|
||||
buildReferrers(f)
|
||||
buildDomTree(f)
|
||||
buildPostDomTree(f)
|
||||
|
||||
if f.Prog.mode&NaiveForm == 0 {
|
||||
for lift(f) {
|
||||
}
|
||||
if doSimplifyConstantCompositeValues {
|
||||
for simplifyConstantCompositeValues(f) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// emit constants after lifting, because lifting may produce new constants, but before other variable splitting,
|
||||
// because it expects constants to have been deduplicated.
|
||||
f.emitConsts()
|
||||
|
||||
if f.Prog.mode&SplitAfterNewInformation != 0 {
|
||||
splitOnNewInformation(f.Blocks[0], &StackMap{})
|
||||
}
|
||||
|
||||
f.namedResults = nil // (used by lifting)
|
||||
f.implicitResults = nil
|
||||
|
||||
numberNodes(f)
|
||||
|
||||
defer f.wr.Close()
|
||||
f.wr.WriteFunc("start", "start", f)
|
||||
|
||||
if f.Prog.mode&PrintFunctions != 0 {
|
||||
printMu.Lock()
|
||||
f.WriteTo(os.Stdout)
|
||||
printMu.Unlock()
|
||||
}
|
||||
|
||||
if f.Prog.mode&SanityCheckFunctions != 0 {
|
||||
mustSanityCheck(f, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func isUselessPhi(phi *Phi) (Value, bool) {
|
||||
var v0 Value
|
||||
for _, e := range phi.Edges {
|
||||
if e == phi {
|
||||
continue
|
||||
}
|
||||
if v0 == nil {
|
||||
v0 = e
|
||||
}
|
||||
if v0 != e {
|
||||
if v0, ok := v0.(*Const); ok {
|
||||
if e, ok := e.(*Const); ok {
|
||||
if v0.typ == e.typ && v0.Value == e.Value {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return v0, true
|
||||
}
|
||||
|
||||
func (f *Function) RemoveNilBlocks() {
|
||||
f.removeNilBlocks()
|
||||
}
|
||||
|
||||
// removeNilBlocks eliminates nils from f.Blocks and updates each
|
||||
// BasicBlock.Index. Use this after any pass that may delete blocks.
|
||||
func (f *Function) removeNilBlocks() {
|
||||
j := 0
|
||||
for _, b := range f.Blocks {
|
||||
if b != nil {
|
||||
b.Index = j
|
||||
f.Blocks[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
// Nil out f.Blocks[j:] to aid GC.
|
||||
for i := j; i < len(f.Blocks); i++ {
|
||||
f.Blocks[i] = nil
|
||||
}
|
||||
f.Blocks = f.Blocks[:j]
|
||||
}
|
||||
|
||||
// SetDebugMode sets the debug mode for package pkg. If true, all its
|
||||
// functions will include full debug info. This greatly increases the
|
||||
// size of the instruction stream, and causes Functions to depend upon
|
||||
// the ASTs, potentially keeping them live in memory for longer.
|
||||
func (pkg *Package) SetDebugMode(debug bool) {
|
||||
// TODO(adonovan): do we want ast.File granularity?
|
||||
pkg.debug = debug
|
||||
}
|
||||
|
||||
// debugInfo reports whether debug info is wanted for this function.
|
||||
func (f *Function) debugInfo() bool {
|
||||
return f.Pkg != nil && f.Pkg.debug
|
||||
}
|
||||
|
||||
// addNamedLocal creates a local variable, adds it to function f and
|
||||
// returns it. Its name and type are taken from obj. Subsequent
|
||||
// calls to f.lookup(obj) will return the same local.
|
||||
func (f *Function) addNamedLocal(obj types.Object, source ast.Node) *Alloc {
|
||||
l := f.addLocal(obj.Type(), source)
|
||||
f.objects[obj] = l
|
||||
return l
|
||||
}
|
||||
|
||||
func (f *Function) addLocalForIdent(id *ast.Ident) *Alloc {
|
||||
return f.addNamedLocal(f.Pkg.info.Defs[id], id)
|
||||
}
|
||||
|
||||
// addLocal creates an anonymous local variable of type typ, adds it
|
||||
// to function f and returns it. pos is the optional source location.
|
||||
func (f *Function) addLocal(typ types.Type, source ast.Node) *Alloc {
|
||||
v := &Alloc{}
|
||||
v.setType(types.NewPointer(typ))
|
||||
f.Locals = append(f.Locals, v)
|
||||
f.emit(v, source)
|
||||
return v
|
||||
}
|
||||
|
||||
// lookup returns the address of the named variable identified by obj
|
||||
// that is local to function f or one of its enclosing functions.
|
||||
// If escaping, the reference comes from a potentially escaping pointer
|
||||
// expression and the referent must be heap-allocated.
|
||||
func (f *Function) lookup(obj types.Object, escaping bool) Value {
|
||||
if v, ok := f.objects[obj]; ok {
|
||||
if alloc, ok := v.(*Alloc); ok && escaping {
|
||||
alloc.Heap = true
|
||||
}
|
||||
return v // function-local var (address)
|
||||
}
|
||||
|
||||
// Definition must be in an enclosing function;
|
||||
// plumb it through intervening closures.
|
||||
if f.parent == nil {
|
||||
panic("no ir.Value for " + obj.String())
|
||||
}
|
||||
outer := f.parent.lookup(obj, true) // escaping
|
||||
v := &FreeVar{
|
||||
name: obj.Name(),
|
||||
typ: outer.Type(),
|
||||
outer: outer,
|
||||
parent: f,
|
||||
}
|
||||
f.objects[obj] = v
|
||||
f.FreeVars = append(f.FreeVars, v)
|
||||
return v
|
||||
}
|
||||
|
||||
// emit emits the specified instruction to function f.
|
||||
func (f *Function) emit(instr Instruction, source ast.Node) Value {
|
||||
return f.currentBlock.emit(instr, source)
|
||||
}
|
||||
|
||||
// RelString returns the full name of this function, qualified by
|
||||
// package name, receiver type, etc.
|
||||
//
|
||||
// The specific formatting rules are not guaranteed and may change.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "math.IsNaN" // a package-level function
|
||||
// "(*bytes.Buffer).Bytes" // a declared method or a wrapper
|
||||
// "(*bytes.Buffer).Bytes$thunk" // thunk (func wrapping method; receiver is param 0)
|
||||
// "(*bytes.Buffer).Bytes$bound" // bound (func wrapping method; receiver supplied by closure)
|
||||
// "main.main$1" // an anonymous function in main
|
||||
// "main.init#1" // a declared init function
|
||||
// "main.init" // the synthesized package initializer
|
||||
//
|
||||
// When these functions are referred to from within the same package
|
||||
// (i.e. from == f.Pkg.Object), they are rendered without the package path.
|
||||
// For example: "IsNaN", "(*Buffer).Bytes", etc.
|
||||
//
|
||||
// All non-synthetic functions have distinct package-qualified names.
|
||||
// (But two methods may have the same name "(T).f" if one is a synthetic
|
||||
// wrapper promoting a non-exported method "f" from another package; in
|
||||
// that case, the strings are equal but the identifiers "f" are distinct.)
|
||||
func (f *Function) RelString(from *types.Package) string {
|
||||
// Anonymous?
|
||||
if f.parent != nil {
|
||||
// An anonymous function's Name() looks like "parentName$1",
|
||||
// but its String() should include the type/package/etc.
|
||||
parent := f.parent.RelString(from)
|
||||
for i, anon := range f.parent.AnonFuncs {
|
||||
if anon == f {
|
||||
return fmt.Sprintf("%s$%d", parent, 1+i)
|
||||
}
|
||||
}
|
||||
|
||||
return f.name // should never happen
|
||||
}
|
||||
|
||||
// Method (declared or wrapper)?
|
||||
if recv := f.Signature.Recv(); recv != nil {
|
||||
return f.relMethod(from, recv.Type())
|
||||
}
|
||||
|
||||
// Thunk?
|
||||
if f.method != nil {
|
||||
return f.relMethod(from, f.method.Recv())
|
||||
}
|
||||
|
||||
// Bound?
|
||||
if len(f.FreeVars) == 1 && strings.HasSuffix(f.name, "$bound") {
|
||||
return f.relMethod(from, f.FreeVars[0].Type())
|
||||
}
|
||||
|
||||
// Package-level function?
|
||||
// Prefix with package name for cross-package references only.
|
||||
if p := f.pkg(); p != nil && p != from {
|
||||
return fmt.Sprintf("%s.%s", p.Path(), f.name)
|
||||
}
|
||||
|
||||
// Unknown.
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *Function) relMethod(from *types.Package, recv types.Type) string {
|
||||
return fmt.Sprintf("(%s).%s", relType(recv, from), f.name)
|
||||
}
|
||||
|
||||
// writeSignature writes to buf the signature sig in declaration syntax.
|
||||
func writeSignature(buf *bytes.Buffer, from *types.Package, name string, sig *types.Signature, params []*Parameter) {
|
||||
buf.WriteString("func ")
|
||||
if recv := sig.Recv(); recv != nil {
|
||||
buf.WriteString("(")
|
||||
if n := params[0].Name(); n != "" {
|
||||
buf.WriteString(n)
|
||||
buf.WriteString(" ")
|
||||
}
|
||||
types.WriteType(buf, params[0].Type(), types.RelativeTo(from))
|
||||
buf.WriteString(") ")
|
||||
}
|
||||
buf.WriteString(name)
|
||||
types.WriteSignature(buf, sig, types.RelativeTo(from))
|
||||
}
|
||||
|
||||
func (f *Function) pkg() *types.Package {
|
||||
if f.Pkg != nil {
|
||||
return f.Pkg.Pkg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ io.WriterTo = (*Function)(nil) // *Function implements io.Writer
|
||||
|
||||
func (f *Function) WriteTo(w io.Writer) (int64, error) {
|
||||
var buf bytes.Buffer
|
||||
WriteFunction(&buf, f)
|
||||
n, err := w.Write(buf.Bytes())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// WriteFunction writes to buf a human-readable "disassembly" of f.
|
||||
func WriteFunction(buf *bytes.Buffer, f *Function) {
|
||||
fmt.Fprintf(buf, "# Name: %s\n", f.String())
|
||||
if f.Pkg != nil {
|
||||
fmt.Fprintf(buf, "# Package: %s\n", f.Pkg.Pkg.Path())
|
||||
}
|
||||
if syn := f.Synthetic; syn != 0 {
|
||||
fmt.Fprintln(buf, "# Synthetic:", syn)
|
||||
}
|
||||
if pos := f.Pos(); pos.IsValid() {
|
||||
fmt.Fprintf(buf, "# Location: %s\n", f.Prog.Fset.Position(pos))
|
||||
}
|
||||
|
||||
if f.parent != nil {
|
||||
fmt.Fprintf(buf, "# Parent: %s\n", f.parent.Name())
|
||||
}
|
||||
|
||||
from := f.pkg()
|
||||
|
||||
if f.FreeVars != nil {
|
||||
buf.WriteString("# Free variables:\n")
|
||||
for i, fv := range f.FreeVars {
|
||||
fmt.Fprintf(buf, "# % 3d:\t%s %s\n", i, fv.Name(), relType(fv.Type(), from))
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.Locals) > 0 {
|
||||
buf.WriteString("# Locals:\n")
|
||||
for i, l := range f.Locals {
|
||||
fmt.Fprintf(buf, "# % 3d:\t%s %s\n", i, l.Name(), relType(deref(l.Type()), from))
|
||||
}
|
||||
}
|
||||
writeSignature(buf, from, f.Name(), f.Signature, f.Params)
|
||||
buf.WriteString(":\n")
|
||||
|
||||
if f.Blocks == nil {
|
||||
buf.WriteString("\t(external)\n")
|
||||
}
|
||||
|
||||
for _, b := range f.Blocks {
|
||||
if b == nil {
|
||||
// Corrupt CFG.
|
||||
fmt.Fprintf(buf, ".nil:\n")
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(buf, "b%d:", b.Index)
|
||||
if len(b.Preds) > 0 {
|
||||
fmt.Fprint(buf, " ←")
|
||||
for _, pred := range b.Preds {
|
||||
fmt.Fprintf(buf, " b%d", pred.Index)
|
||||
}
|
||||
}
|
||||
if b.Comment != "" {
|
||||
fmt.Fprintf(buf, " # %s", b.Comment)
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
|
||||
if false { // CFG debugging
|
||||
fmt.Fprintf(buf, "\t# CFG: %s --> %s --> %s\n", b.Preds, b, b.Succs)
|
||||
}
|
||||
|
||||
buf2 := &bytes.Buffer{}
|
||||
for _, instr := range b.Instrs {
|
||||
buf.WriteString("\t")
|
||||
switch v := instr.(type) {
|
||||
case Value:
|
||||
// Left-align the instruction.
|
||||
if name := v.Name(); name != "" {
|
||||
fmt.Fprintf(buf, "%s = ", name)
|
||||
}
|
||||
buf.WriteString(instr.String())
|
||||
case nil:
|
||||
// Be robust against bad transforms.
|
||||
buf.WriteString("<deleted>")
|
||||
default:
|
||||
buf.WriteString(instr.String())
|
||||
}
|
||||
if instr != nil && instr.Comment() != "" {
|
||||
buf.WriteString(" # ")
|
||||
buf.WriteString(instr.Comment())
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
|
||||
if f.Prog.mode&PrintSource != 0 {
|
||||
if s := instr.Source(); s != nil {
|
||||
buf2.Reset()
|
||||
format.Node(buf2, f.Prog.Fset, s)
|
||||
for {
|
||||
line, err := buf2.ReadString('\n')
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
buf.WriteString("\t\t> ")
|
||||
buf.WriteString(line)
|
||||
if line[len(line)-1] != '\n' {
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// newBasicBlock adds to f a new basic block and returns it. It does
|
||||
// not automatically become the current block for subsequent calls to emit.
|
||||
// comment is an optional string for more readable debugging output.
|
||||
func (f *Function) newBasicBlock(comment string) *BasicBlock {
|
||||
var instrs []Instruction
|
||||
if len(f.functionBody.scratchInstructions) > 0 {
|
||||
instrs = f.functionBody.scratchInstructions[0:0:avgInstructionsPerBlock]
|
||||
f.functionBody.scratchInstructions = f.functionBody.scratchInstructions[avgInstructionsPerBlock:]
|
||||
} else {
|
||||
instrs = make([]Instruction, 0, avgInstructionsPerBlock)
|
||||
}
|
||||
|
||||
b := &BasicBlock{
|
||||
Index: len(f.Blocks),
|
||||
Comment: comment,
|
||||
parent: f,
|
||||
Instrs: instrs,
|
||||
}
|
||||
b.Succs = b.succs2[:0]
|
||||
f.Blocks = append(f.Blocks, b)
|
||||
return b
|
||||
}
|
||||
|
||||
// NewFunction returns a new synthetic Function instance belonging to
|
||||
// prog, with its name and signature fields set as specified.
|
||||
//
|
||||
// The caller is responsible for initializing the remaining fields of
|
||||
// the function object, e.g. Pkg, Params, Blocks.
|
||||
//
|
||||
// It is practically impossible for clients to construct well-formed
|
||||
// IR functions/packages/programs directly, so we assume this is the
|
||||
// job of the Builder alone. NewFunction exists to provide clients a
|
||||
// little flexibility. For example, analysis tools may wish to
|
||||
// construct fake Functions for the root of the callgraph, a fake
|
||||
// "reflect" package, etc.
|
||||
//
|
||||
// TODO(adonovan): think harder about the API here.
|
||||
func (prog *Program) NewFunction(name string, sig *types.Signature, provenance Synthetic) *Function {
|
||||
return &Function{Prog: prog, name: name, Signature: sig, Synthetic: provenance}
|
||||
}
|
||||
|
||||
//lint:ignore U1000 we may make use of this for functions loaded from export data
|
||||
type extentNode [2]token.Pos
|
||||
|
||||
func (n extentNode) Pos() token.Pos { return n[0] }
|
||||
func (n extentNode) End() token.Pos { return n[1] }
|
||||
|
||||
func (f *Function) initHTML(name string) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
if rel := f.RelString(nil); rel == name {
|
||||
f.wr = NewHTMLWriter("ir.html", rel, "")
|
||||
}
|
||||
}
|
||||
|
||||
func killInstruction(instr Instruction) {
|
||||
ops := instr.Operands(nil)
|
||||
for _, op := range ops {
|
||||
if refs := (*op).Referrers(); refs != nil {
|
||||
*refs = removeInstr(*refs, instr)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package irutil
|
||||
|
||||
// This file defines utility functions for constructing programs in IR form.
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
|
||||
//lint:ignore SA1019 go/loader is deprecated, but works fine for our tests
|
||||
"golang.org/x/tools/go/loader"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// Which function, if any, to print in HTML form
|
||||
PrintFunc string
|
||||
}
|
||||
|
||||
// Packages creates an IR program for a set of packages.
|
||||
//
|
||||
// The packages must have been loaded from source syntax using the
|
||||
// golang.org/x/tools/go/packages.Load function in LoadSyntax or
|
||||
// LoadAllSyntax mode.
|
||||
//
|
||||
// Packages creates an IR package for each well-typed package in the
|
||||
// initial list, plus all their dependencies. The resulting list of
|
||||
// packages corresponds to the list of initial packages, and may contain
|
||||
// a nil if IR code could not be constructed for the corresponding initial
|
||||
// package due to type errors.
|
||||
//
|
||||
// Code for bodies of functions is not built until Build is called on
|
||||
// the resulting Program. IR code is constructed only for the initial
|
||||
// packages with well-typed syntax trees.
|
||||
//
|
||||
// The mode parameter controls diagnostics and checking during IR construction.
|
||||
func Packages(initial []*packages.Package, mode ir.BuilderMode, opts *Options) (*ir.Program, []*ir.Package) {
|
||||
return doPackages(initial, mode, false, opts)
|
||||
}
|
||||
|
||||
// AllPackages creates an IR program for a set of packages plus all
|
||||
// their dependencies.
|
||||
//
|
||||
// The packages must have been loaded from source syntax using the
|
||||
// golang.org/x/tools/go/packages.Load function in LoadAllSyntax mode.
|
||||
//
|
||||
// AllPackages creates an IR package for each well-typed package in the
|
||||
// initial list, plus all their dependencies. The resulting list of
|
||||
// packages corresponds to the list of initial packages, and may contain
|
||||
// a nil if IR code could not be constructed for the corresponding
|
||||
// initial package due to type errors.
|
||||
//
|
||||
// Code for bodies of functions is not built until Build is called on
|
||||
// the resulting Program. IR code is constructed for all packages with
|
||||
// well-typed syntax trees.
|
||||
//
|
||||
// The mode parameter controls diagnostics and checking during IR construction.
|
||||
func AllPackages(initial []*packages.Package, mode ir.BuilderMode, opts *Options) (*ir.Program, []*ir.Package) {
|
||||
return doPackages(initial, mode, true, opts)
|
||||
}
|
||||
|
||||
func doPackages(initial []*packages.Package, mode ir.BuilderMode, deps bool, opts *Options) (*ir.Program, []*ir.Package) {
|
||||
|
||||
var fset *token.FileSet
|
||||
if len(initial) > 0 {
|
||||
fset = initial[0].Fset
|
||||
}
|
||||
|
||||
prog := ir.NewProgram(fset, mode)
|
||||
if opts != nil {
|
||||
prog.PrintFunc = opts.PrintFunc
|
||||
}
|
||||
|
||||
isInitial := make(map[*packages.Package]bool, len(initial))
|
||||
for _, p := range initial {
|
||||
isInitial[p] = true
|
||||
}
|
||||
|
||||
irmap := make(map[*packages.Package]*ir.Package)
|
||||
packages.Visit(initial, nil, func(p *packages.Package) {
|
||||
if p.Types != nil && !p.IllTyped {
|
||||
var files []*ast.File
|
||||
if deps || isInitial[p] {
|
||||
files = p.Syntax
|
||||
}
|
||||
irmap[p] = prog.CreatePackage(p.Types, files, p.TypesInfo, true)
|
||||
}
|
||||
})
|
||||
|
||||
var irpkgs []*ir.Package
|
||||
for _, p := range initial {
|
||||
irpkgs = append(irpkgs, irmap[p]) // may be nil
|
||||
}
|
||||
return prog, irpkgs
|
||||
}
|
||||
|
||||
// CreateProgram returns a new program in IR form, given a program
|
||||
// loaded from source. An IR package is created for each transitively
|
||||
// error-free package of lprog.
|
||||
//
|
||||
// Code for bodies of functions is not built until Build is called
|
||||
// on the result.
|
||||
//
|
||||
// The mode parameter controls diagnostics and checking during IR construction.
|
||||
//
|
||||
// Deprecated: use golang.org/x/tools/go/packages and the Packages
|
||||
// function instead; see ir.ExampleLoadPackages.
|
||||
func CreateProgram(lprog *loader.Program, mode ir.BuilderMode) *ir.Program {
|
||||
prog := ir.NewProgram(lprog.Fset, mode)
|
||||
|
||||
for _, info := range lprog.AllPackages {
|
||||
if info.TransitivelyErrorFree {
|
||||
prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable)
|
||||
}
|
||||
}
|
||||
|
||||
return prog
|
||||
}
|
||||
|
||||
// BuildPackage builds an IR program with IR for a single package.
|
||||
//
|
||||
// It populates pkg by type-checking the specified file ASTs. All
|
||||
// dependencies are loaded using the importer specified by tc, which
|
||||
// typically loads compiler export data; IR code cannot be built for
|
||||
// those packages. BuildPackage then constructs an ir.Program with all
|
||||
// dependency packages created, and builds and returns the IR package
|
||||
// corresponding to pkg.
|
||||
//
|
||||
// The caller must have set pkg.Path() to the import path.
|
||||
//
|
||||
// The operation fails if there were any type-checking or import errors.
|
||||
//
|
||||
// See ../ir/example_test.go for an example.
|
||||
func BuildPackage(tc *types.Config, fset *token.FileSet, pkg *types.Package, files []*ast.File, mode ir.BuilderMode) (*ir.Package, *types.Info, error) {
|
||||
if fset == nil {
|
||||
panic("no token.FileSet")
|
||||
}
|
||||
if pkg.Path() == "" {
|
||||
panic("package has no import path")
|
||||
}
|
||||
|
||||
info := &types.Info{
|
||||
Types: make(map[ast.Expr]types.TypeAndValue),
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
Uses: make(map[*ast.Ident]types.Object),
|
||||
Implicits: make(map[ast.Node]types.Object),
|
||||
Scopes: make(map[ast.Node]*types.Scope),
|
||||
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
||||
}
|
||||
if err := types.NewChecker(tc, fset, pkg, info).Files(files); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
prog := ir.NewProgram(fset, mode)
|
||||
|
||||
// Create IR packages for all imports.
|
||||
// Order is not significant.
|
||||
created := make(map[*types.Package]bool)
|
||||
var createAll func(pkgs []*types.Package)
|
||||
createAll = func(pkgs []*types.Package) {
|
||||
for _, p := range pkgs {
|
||||
if !created[p] {
|
||||
created[p] = true
|
||||
prog.CreatePackage(p, nil, nil, true)
|
||||
createAll(p.Imports())
|
||||
}
|
||||
}
|
||||
}
|
||||
createAll(pkg.Imports())
|
||||
|
||||
// Create and build the primary package.
|
||||
irpkg := prog.CreatePackage(pkg, files, info, false)
|
||||
irpkg.Build()
|
||||
return irpkg, info, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package irutil_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
const hello = `package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, world")
|
||||
}
|
||||
`
|
||||
|
||||
func TestBuildPackage(t *testing.T) {
|
||||
// There is a more substantial test of BuildPackage and the
|
||||
// IR program it builds in ../ir/builder_test.go.
|
||||
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "hello.go", hello, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkg := types.NewPackage("hello", "")
|
||||
irpkg, _, err := irutil.BuildPackage(&types.Config{Importer: importer.Default()}, fset, pkg, []*ast.File{f}, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pkg.Name() != "main" {
|
||||
t.Errorf("pkg.Name() = %s, want main", pkg.Name())
|
||||
}
|
||||
if irpkg.Func("main") == nil {
|
||||
irpkg.WriteTo(os.Stderr)
|
||||
t.Errorf("irpkg has no main function")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackages(t *testing.T) {
|
||||
cfg := &packages.Config{Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
initial, err := packages.Load(cfg, "bytes")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if packages.PrintErrors(initial) > 0 {
|
||||
t.Fatal("there were errors")
|
||||
}
|
||||
|
||||
prog, pkgs := irutil.Packages(initial, 0, nil)
|
||||
bytesNewBuffer := pkgs[0].Func("NewBuffer")
|
||||
bytesNewBuffer.Pkg.Build()
|
||||
|
||||
// We'll dump the IR of bytes.NewBuffer because it is small and stable.
|
||||
out := new(bytes.Buffer)
|
||||
bytesNewBuffer.WriteTo(out)
|
||||
|
||||
// For determinism, sanitize the location.
|
||||
location := prog.Fset.Position(bytesNewBuffer.Pos()).String()
|
||||
got := strings.Replace(out.String(), location, "$GOROOT/src/bytes/buffer.go:1", -1)
|
||||
|
||||
want := `
|
||||
# Name: bytes.NewBuffer
|
||||
# Package: bytes
|
||||
# Location: $GOROOT/src/bytes/buffer.go:1
|
||||
func NewBuffer(buf []byte) *Buffer:
|
||||
b0: # entry
|
||||
t1 = Const <int> {0}
|
||||
t2 = Const <readOp> {0}
|
||||
t3 = Parameter <[]byte> {buf}
|
||||
t4 = HeapAlloc <*Buffer>
|
||||
t5 = CompositeValue <Buffer> [100] t3 t1 t2
|
||||
Store {bytes.Buffer} t4 t5
|
||||
Jump → b1
|
||||
|
||||
b1: ← b0 # exit
|
||||
Return t4
|
||||
|
||||
`[1:]
|
||||
if got != want {
|
||||
t.Errorf("bytes.NewBuffer IR = <<%s>>, want <<%s>>", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPackage_MissingImport(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "bad.go", `package bad; import "missing"`, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkg := types.NewPackage("bad", "")
|
||||
irpkg, _, err := irutil.BuildPackage(new(types.Config), fset, pkg, []*ast.File{f}, 0)
|
||||
if err == nil || irpkg != nil {
|
||||
t.Fatal("BuildPackage succeeded unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue28106(t *testing.T) {
|
||||
// In go1.10, go/packages loads all packages from source, not
|
||||
// export data, but does not type check function bodies of
|
||||
// imported packages. This test ensures that we do not attempt
|
||||
// to run the IR builder on functions without type information.
|
||||
cfg := &packages.Config{Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
pkgs, err := packages.Load(cfg, "runtime")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prog, _ := irutil.Packages(pkgs, 0, nil)
|
||||
prog.Build() // no crash
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package irutil
|
||||
|
||||
import "honnef.co/go/tools/go/ir"
|
||||
|
||||
type Loop struct{ *ir.BlockSet }
|
||||
|
||||
func FindLoops(fn *ir.Function) []Loop {
|
||||
if fn.Blocks == nil {
|
||||
return nil
|
||||
}
|
||||
tree := fn.DomPreorder()
|
||||
var sets []Loop
|
||||
for _, h := range tree {
|
||||
for _, n := range h.Preds {
|
||||
if !h.Dominates(n) {
|
||||
continue
|
||||
}
|
||||
// n is a back-edge to h
|
||||
// h is the loop header
|
||||
if n == h {
|
||||
set := Loop{ir.NewBlockSet(len(fn.Blocks))}
|
||||
set.Add(n)
|
||||
sets = append(sets, set)
|
||||
continue
|
||||
}
|
||||
set := Loop{ir.NewBlockSet(len(fn.Blocks))}
|
||||
set.Add(h)
|
||||
set.Add(n)
|
||||
for _, b := range allPredsBut(n, h, nil) {
|
||||
set.Add(b)
|
||||
}
|
||||
sets = append(sets, set)
|
||||
}
|
||||
}
|
||||
return sets
|
||||
}
|
||||
|
||||
func allPredsBut(b, but *ir.BasicBlock, list []*ir.BasicBlock) []*ir.BasicBlock {
|
||||
outer:
|
||||
for _, pred := range b.Preds {
|
||||
if pred == but {
|
||||
continue
|
||||
}
|
||||
for _, p := range list {
|
||||
// TODO improve big-o complexity of this function
|
||||
if pred == p {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
list = append(list, pred)
|
||||
list = allPredsBut(pred, but, list)
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package irutil
|
||||
|
||||
import (
|
||||
"honnef.co/go/tools/go/ir"
|
||||
)
|
||||
|
||||
// IsStub reports whether a function is a stub. A function is
|
||||
// considered a stub if it has no instructions or if all it does is
|
||||
// return a constant value.
|
||||
func IsStub(fn *ir.Function) bool {
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
switch instr.(type) {
|
||||
case *ir.Const:
|
||||
// const naturally has no side-effects
|
||||
case *ir.Panic:
|
||||
// panic is a stub if it only uses constants
|
||||
case *ir.Return:
|
||||
// return is a stub if it only uses constants
|
||||
case *ir.DebugRef:
|
||||
case *ir.Jump:
|
||||
// if there are no disallowed instructions, then we're
|
||||
// only jumping to the exit block (or possibly
|
||||
// somewhere else that's stubby?)
|
||||
default:
|
||||
// all other instructions are assumed to do actual work
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package irutil
|
||||
|
||||
// This file implements discovery of switch and type-switch constructs
|
||||
// from low-level control flow.
|
||||
//
|
||||
// Many techniques exist for compiling a high-level switch with
|
||||
// constant cases to efficient machine code. The optimal choice will
|
||||
// depend on the data type, the specific case values, the code in the
|
||||
// body of each case, and the hardware.
|
||||
// Some examples:
|
||||
// - a lookup table (for a switch that maps constants to constants)
|
||||
// - a computed goto
|
||||
// - a binary tree
|
||||
// - a perfect hash
|
||||
// - a two-level switch (to partition constant strings by their first byte).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
)
|
||||
|
||||
// A ConstCase represents a single constant comparison.
|
||||
// It is part of a Switch.
|
||||
type ConstCase struct {
|
||||
Block *ir.BasicBlock // block performing the comparison
|
||||
Body *ir.BasicBlock // body of the case
|
||||
Value *ir.Const // case comparand
|
||||
}
|
||||
|
||||
// A TypeCase represents a single type assertion.
|
||||
// It is part of a Switch.
|
||||
type TypeCase struct {
|
||||
Block *ir.BasicBlock // block performing the type assert
|
||||
Body *ir.BasicBlock // body of the case
|
||||
Type types.Type // case type
|
||||
Binding ir.Value // value bound by this case
|
||||
}
|
||||
|
||||
// A Switch is a logical high-level control flow operation
|
||||
// (a multiway branch) discovered by analysis of a CFG containing
|
||||
// only if/else chains. It is not part of the ir.Instruction set.
|
||||
//
|
||||
// One of ConstCases and TypeCases has length >= 2;
|
||||
// the other is nil.
|
||||
//
|
||||
// In a value switch, the list of cases may contain duplicate constants.
|
||||
// A type switch may contain duplicate types, or types assignable
|
||||
// to an interface type also in the list.
|
||||
// TODO(adonovan): eliminate such duplicates.
|
||||
type Switch struct {
|
||||
Start *ir.BasicBlock // block containing start of if/else chain
|
||||
X ir.Value // the switch operand
|
||||
ConstCases []ConstCase // ordered list of constant comparisons
|
||||
TypeCases []TypeCase // ordered list of type assertions
|
||||
Default *ir.BasicBlock // successor if all comparisons fail
|
||||
}
|
||||
|
||||
func (sw *Switch) String() string {
|
||||
// We represent each block by the String() of its
|
||||
// first Instruction, e.g. "print(42:int)".
|
||||
var buf bytes.Buffer
|
||||
if sw.ConstCases != nil {
|
||||
fmt.Fprintf(&buf, "switch %s {\n", sw.X.Name())
|
||||
for _, c := range sw.ConstCases {
|
||||
fmt.Fprintf(&buf, "case %s: %s\n", c.Value.Name(), c.Body.Instrs[0])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "switch %s.(type) {\n", sw.X.Name())
|
||||
for _, c := range sw.TypeCases {
|
||||
fmt.Fprintf(&buf, "case %s %s: %s\n",
|
||||
c.Binding.Name(), c.Type, c.Body.Instrs[0])
|
||||
}
|
||||
}
|
||||
if sw.Default != nil {
|
||||
fmt.Fprintf(&buf, "default: %s\n", sw.Default.Instrs[0])
|
||||
}
|
||||
fmt.Fprintf(&buf, "}")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Switches examines the control-flow graph of fn and returns the
|
||||
// set of inferred value and type switches. A value switch tests an
|
||||
// ir.Value for equality against two or more compile-time constant
|
||||
// values. Switches involving link-time constants (addresses) are
|
||||
// ignored. A type switch type-asserts an ir.Value against two or
|
||||
// more types.
|
||||
//
|
||||
// The switches are returned in dominance order.
|
||||
//
|
||||
// The resulting switches do not necessarily correspond to uses of the
|
||||
// 'switch' keyword in the source: for example, a single source-level
|
||||
// switch statement with non-constant cases may result in zero, one or
|
||||
// many Switches, one per plural sequence of constant cases.
|
||||
// Switches may even be inferred from if/else- or goto-based control flow.
|
||||
// (In general, the control flow constructs of the source program
|
||||
// cannot be faithfully reproduced from the IR.)
|
||||
func Switches(fn *ir.Function) []Switch {
|
||||
// Traverse the CFG in dominance order, so we don't
|
||||
// enter an if/else-chain in the middle.
|
||||
var switches []Switch
|
||||
seen := make(map[*ir.BasicBlock]bool) // TODO(adonovan): opt: use ir.blockSet
|
||||
for _, b := range fn.DomPreorder() {
|
||||
if x, k := isComparisonBlock(b); x != nil {
|
||||
// Block b starts a switch.
|
||||
sw := Switch{Start: b, X: x}
|
||||
valueSwitch(&sw, k, seen)
|
||||
if len(sw.ConstCases) > 1 {
|
||||
switches = append(switches, sw)
|
||||
}
|
||||
}
|
||||
|
||||
if y, x, T := isTypeAssertBlock(b); y != nil {
|
||||
// Block b starts a type switch.
|
||||
sw := Switch{Start: b, X: x}
|
||||
typeSwitch(&sw, y, T, seen)
|
||||
if len(sw.TypeCases) > 1 {
|
||||
switches = append(switches, sw)
|
||||
}
|
||||
}
|
||||
}
|
||||
return switches
|
||||
}
|
||||
|
||||
func isSameX(x1 ir.Value, x2 ir.Value) bool {
|
||||
if x1 == x2 {
|
||||
return true
|
||||
}
|
||||
if x2, ok := x2.(*ir.Sigma); ok {
|
||||
return isSameX(x1, x2.X)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func valueSwitch(sw *Switch, k *ir.Const, seen map[*ir.BasicBlock]bool) {
|
||||
b := sw.Start
|
||||
x := sw.X
|
||||
for isSameX(sw.X, x) {
|
||||
if seen[b] {
|
||||
break
|
||||
}
|
||||
seen[b] = true
|
||||
|
||||
sw.ConstCases = append(sw.ConstCases, ConstCase{
|
||||
Block: b,
|
||||
Body: b.Succs[0],
|
||||
Value: k,
|
||||
})
|
||||
b = b.Succs[1]
|
||||
n := 0
|
||||
for _, instr := range b.Instrs {
|
||||
switch instr.(type) {
|
||||
case *ir.If, *ir.BinOp:
|
||||
n++
|
||||
case *ir.Sigma, *ir.Phi, *ir.DebugRef:
|
||||
default:
|
||||
n += 1000
|
||||
}
|
||||
}
|
||||
if n != 2 {
|
||||
// Block b contains not just 'if x == k' and σ/ϕ nodes,
|
||||
// so it may have side effects that
|
||||
// make it unsafe to elide.
|
||||
break
|
||||
}
|
||||
if len(b.Preds) != 1 {
|
||||
// Block b has multiple predecessors,
|
||||
// so it cannot be treated as a case.
|
||||
break
|
||||
}
|
||||
x, k = isComparisonBlock(b)
|
||||
}
|
||||
sw.Default = b
|
||||
}
|
||||
|
||||
func typeSwitch(sw *Switch, y ir.Value, T types.Type, seen map[*ir.BasicBlock]bool) {
|
||||
b := sw.Start
|
||||
x := sw.X
|
||||
for isSameX(sw.X, x) {
|
||||
if seen[b] {
|
||||
break
|
||||
}
|
||||
seen[b] = true
|
||||
|
||||
sw.TypeCases = append(sw.TypeCases, TypeCase{
|
||||
Block: b,
|
||||
Body: b.Succs[0],
|
||||
Type: T,
|
||||
Binding: y,
|
||||
})
|
||||
b = b.Succs[1]
|
||||
n := 0
|
||||
for _, instr := range b.Instrs {
|
||||
switch instr.(type) {
|
||||
case *ir.TypeAssert, *ir.Extract, *ir.If:
|
||||
n++
|
||||
case *ir.Sigma, *ir.Phi:
|
||||
default:
|
||||
n += 1000
|
||||
}
|
||||
}
|
||||
if n != 4 {
|
||||
// Block b contains not just
|
||||
// {TypeAssert; Extract #0; Extract #1; If}
|
||||
// so it may have side effects that
|
||||
// make it unsafe to elide.
|
||||
break
|
||||
}
|
||||
if len(b.Preds) != 1 {
|
||||
// Block b has multiple predecessors,
|
||||
// so it cannot be treated as a case.
|
||||
break
|
||||
}
|
||||
y, x, T = isTypeAssertBlock(b)
|
||||
}
|
||||
sw.Default = b
|
||||
}
|
||||
|
||||
// isComparisonBlock returns the operands (v, k) if a block ends with
|
||||
// a comparison v==k, where k is a compile-time constant.
|
||||
func isComparisonBlock(b *ir.BasicBlock) (v ir.Value, k *ir.Const) {
|
||||
if n := len(b.Instrs); n >= 2 {
|
||||
if i, ok := b.Instrs[n-1].(*ir.If); ok {
|
||||
if binop, ok := i.Cond.(*ir.BinOp); ok && binop.Block() == b && binop.Op == token.EQL {
|
||||
if k, ok := binop.Y.(*ir.Const); ok {
|
||||
return binop.X, k
|
||||
}
|
||||
if k, ok := binop.X.(*ir.Const); ok {
|
||||
return binop.Y, k
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// isTypeAssertBlock returns the operands (y, x, T) if a block ends with
|
||||
// a type assertion "if y, ok := x.(T); ok {".
|
||||
func isTypeAssertBlock(b *ir.BasicBlock) (y, x ir.Value, T types.Type) {
|
||||
if n := len(b.Instrs); n >= 4 {
|
||||
if i, ok := b.Instrs[n-1].(*ir.If); ok {
|
||||
if ext1, ok := i.Cond.(*ir.Extract); ok && ext1.Block() == b && ext1.Index == 1 {
|
||||
if ta, ok := ext1.Tuple.(*ir.TypeAssert); ok && ta.Block() == b {
|
||||
// hack: relies upon instruction ordering.
|
||||
if ext0, ok := b.Instrs[n-3].(*ir.Extract); ok {
|
||||
return ext0, ta.X, ta.AssertedType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// No testdata on Android.
|
||||
|
||||
//go:build !android
|
||||
// +build !android
|
||||
|
||||
package irutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
//lint:ignore SA1019 go/loader is deprecated, but works fine for our tests
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
func TestSwitches(t *testing.T) {
|
||||
conf := loader.Config{ParserMode: parser.ParseComments}
|
||||
f, err := conf.ParseFile(filepath.Join(analysistest.TestData(), "switches.go"), nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
conf.CreateFromFiles("main", f)
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
prog := CreateProgram(iprog, 0)
|
||||
mainPkg := prog.Package(iprog.Created[0].Pkg)
|
||||
mainPkg.Build()
|
||||
|
||||
for _, mem := range mainPkg.Members {
|
||||
if fn, ok := mem.(*ir.Function); ok {
|
||||
if fn.Synthetic != 0 {
|
||||
continue // e.g. init()
|
||||
}
|
||||
// Each (multi-line) "switch" comment within
|
||||
// this function must match the printed form
|
||||
// of a ConstSwitch.
|
||||
var wantSwitches []string
|
||||
for _, c := range f.Comments {
|
||||
if fn.Source().Pos() <= c.Pos() && c.Pos() < fn.Source().End() {
|
||||
text := strings.TrimSpace(c.Text())
|
||||
if strings.HasPrefix(text, "switch ") {
|
||||
wantSwitches = append(wantSwitches, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switches := Switches(fn)
|
||||
if len(switches) != len(wantSwitches) {
|
||||
t.Errorf("in %s, found %d switches, want %d", fn, len(switches), len(wantSwitches))
|
||||
}
|
||||
for i, sw := range switches {
|
||||
got := sw.testString()
|
||||
if i >= len(wantSwitches) {
|
||||
continue
|
||||
}
|
||||
want := wantSwitches[i]
|
||||
if got != want {
|
||||
t.Errorf("in %s, found switch %d: got <<%s>>, want <<%s>>", fn, i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *Switch) testString() string {
|
||||
// same as the actual String method, but use the second to last
|
||||
// instruction instead, to skip over all the phi and sigma nodes
|
||||
// that SSI produces.
|
||||
var buf bytes.Buffer
|
||||
if sw.ConstCases != nil {
|
||||
fmt.Fprintf(&buf, "switch %s {\n", sw.X.Name())
|
||||
for _, c := range sw.ConstCases {
|
||||
n := len(c.Body.Instrs) - 2
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
fmt.Fprintf(&buf, "case %s: %s\n", c.Value.Name(), c.Body.Instrs[n])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "switch %s.(type) {\n", sw.X.Name())
|
||||
for _, c := range sw.TypeCases {
|
||||
n := len(c.Body.Instrs) - 2
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
fmt.Fprintf(&buf, "case %s %s: %s\n",
|
||||
c.Binding.Name(), c.Type, c.Body.Instrs[n])
|
||||
}
|
||||
}
|
||||
if sw.Default != nil {
|
||||
n := len(sw.Default.Instrs) - 2
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
fmt.Fprintf(&buf, "default: %s\n", sw.Default.Instrs[n])
|
||||
}
|
||||
fmt.Fprintf(&buf, "}")
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package irutil
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
)
|
||||
|
||||
// Terminates reports whether fn is supposed to return, that is if it
|
||||
// has at least one theoretic path that returns from the function.
|
||||
// Explicit panics do not count as terminating.
|
||||
func Terminates(fn *ir.Function) bool {
|
||||
if fn.Blocks == nil {
|
||||
// assuming that a function terminates is the conservative
|
||||
// choice
|
||||
return true
|
||||
}
|
||||
|
||||
for _, block := range fn.Blocks {
|
||||
if _, ok := block.Control().(*ir.Return); ok {
|
||||
if len(block.Preds) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, pred := range block.Preds {
|
||||
switch ctrl := pred.Control().(type) {
|
||||
case *ir.Panic:
|
||||
// explicit panics do not count as terminating
|
||||
case *ir.If:
|
||||
// Check if we got here by receiving from a closed
|
||||
// time.Tick channel – this cannot happen at
|
||||
// runtime and thus doesn't constitute termination
|
||||
iff := ctrl
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
ex, ok := iff.Cond.(*ir.Extract)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if ex.Index != 1 {
|
||||
return true
|
||||
}
|
||||
recv, ok := ex.Tuple.(*ir.Recv)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
call, ok := recv.Chan.(*ir.Call)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
fn, ok := call.Common().Value.(*ir.Function)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
fn2, ok := fn.Object().(*types.Func)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if fn2.FullName() != "time.Tick" {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
// we've reached the exit block
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
// This file is the input to TestSwitches in switch_test.go.
|
||||
// Each multiway conditional with constant or type cases (Switch)
|
||||
// discovered by Switches is printed, and compared with the
|
||||
// comments.
|
||||
//
|
||||
// The body of each case is printed as the value of its first
|
||||
// instruction.
|
||||
|
||||
// -------- Value switches --------
|
||||
|
||||
func four() int { return 4 }
|
||||
|
||||
// A non-constant case makes a switch "impure", but its pure
|
||||
// cases form two separate switches.
|
||||
func SwitchWithNonConstantCase(x int) {
|
||||
// switch t8 {
|
||||
// case t1: Call <()> print t1
|
||||
// case t2: Call <()> print t4
|
||||
// case t3: Call <()> print t4
|
||||
// default: BinOp <bool> {==} t26 t27
|
||||
// }
|
||||
|
||||
// switch t32 {
|
||||
// case t5: Call <()> print t5
|
||||
// case t6: Call <()> print t6
|
||||
// default: Call <()> print t7
|
||||
// }
|
||||
switch x {
|
||||
case 1:
|
||||
print(1)
|
||||
case 2, 3:
|
||||
print(23)
|
||||
case four():
|
||||
print(3)
|
||||
case 5:
|
||||
print(5)
|
||||
case 6:
|
||||
print(6)
|
||||
}
|
||||
print("done")
|
||||
}
|
||||
|
||||
// Switches may be found even where the source
|
||||
// program doesn't have a switch statement.
|
||||
|
||||
func ImplicitSwitches(x, y int) {
|
||||
// switch t12 {
|
||||
// case t1: Call <()> print t4
|
||||
// case t2: Call <()> print t4
|
||||
// default: BinOp <bool> {<} t27 t3
|
||||
// }
|
||||
if x == 1 || 2 == x || x < 5 {
|
||||
print(12)
|
||||
}
|
||||
|
||||
// switch t24 {
|
||||
// case t5: Call <()> print t7
|
||||
// case t6: Call <()> print t7
|
||||
// default: BinOp <bool> {==} t49 t50
|
||||
// }
|
||||
if x == 3 || 4 == x || x == y {
|
||||
print(34)
|
||||
}
|
||||
|
||||
// Not a switch: no consistent variable.
|
||||
if x == 5 || y == 6 {
|
||||
print(56)
|
||||
}
|
||||
|
||||
// Not a switch: only one constant comparison.
|
||||
if x == 7 || x == y {
|
||||
print(78)
|
||||
}
|
||||
}
|
||||
|
||||
func IfElseBasedSwitch(x int) {
|
||||
// switch t4 {
|
||||
// case t1: Call <()> print t1
|
||||
// case t2: Call <()> print t2
|
||||
// default: Call <()> print t3
|
||||
// }
|
||||
if x == 1 {
|
||||
print(1)
|
||||
} else if x == 2 {
|
||||
print(2)
|
||||
} else {
|
||||
print("else")
|
||||
}
|
||||
}
|
||||
|
||||
func GotoBasedSwitch(x int) {
|
||||
// switch t4 {
|
||||
// case t1: Call <()> print t1
|
||||
// case t2: Call <()> print t2
|
||||
// default: Call <()> print t3
|
||||
// }
|
||||
if x == 1 {
|
||||
goto L1
|
||||
}
|
||||
if x == 2 {
|
||||
goto L2
|
||||
}
|
||||
print("else")
|
||||
L1:
|
||||
print(1)
|
||||
goto end
|
||||
L2:
|
||||
print(2)
|
||||
end:
|
||||
}
|
||||
|
||||
func SwitchInAForLoop(x, y int) {
|
||||
// switch t11 {
|
||||
// case t2: Call <()> print t2
|
||||
// case t3: Call <()> print t3
|
||||
// default: BinOp <bool> {==} t29 t28
|
||||
// }
|
||||
loop:
|
||||
for {
|
||||
print("head")
|
||||
switch x {
|
||||
case 1:
|
||||
print(1)
|
||||
break loop
|
||||
case 2:
|
||||
print(2)
|
||||
break loop
|
||||
case y:
|
||||
print(3)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This case is a switch in a for-loop, both constructed using goto.
|
||||
// As before, the default case points back to the block containing the
|
||||
// switch, but that's ok.
|
||||
func SwitchInAForLoopUsingGoto(x int) {
|
||||
// switch t8 {
|
||||
// case t2: Call <()> print t2
|
||||
// case t3: Call <()> print t3
|
||||
// default: BinOp <bool> {==} t8 t2
|
||||
// }
|
||||
loop:
|
||||
print("head")
|
||||
if x == 1 {
|
||||
goto L1
|
||||
}
|
||||
if x == 2 {
|
||||
goto L2
|
||||
}
|
||||
goto loop
|
||||
L1:
|
||||
print(1)
|
||||
goto end
|
||||
L2:
|
||||
print(2)
|
||||
end:
|
||||
}
|
||||
|
||||
func UnstructuredSwitchInAForLoop(x int) {
|
||||
// switch t8 {
|
||||
// case t1: Call <()> print t1
|
||||
// case t2: BinOp <bool> {==} t8 t1
|
||||
// default: Call <()> print t3
|
||||
// }
|
||||
for {
|
||||
if x == 1 {
|
||||
print(1)
|
||||
return
|
||||
}
|
||||
if x == 2 {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
print("end")
|
||||
}
|
||||
|
||||
func CaseWithMultiplePreds(x int) {
|
||||
for {
|
||||
if x == 1 {
|
||||
print(1)
|
||||
return
|
||||
}
|
||||
loop:
|
||||
// This block has multiple predecessors,
|
||||
// so can't be treated as a switch case.
|
||||
if x == 2 {
|
||||
goto loop
|
||||
}
|
||||
break
|
||||
}
|
||||
print("end")
|
||||
}
|
||||
|
||||
func DuplicateConstantsAreNotEliminated(x int) {
|
||||
// switch t4 {
|
||||
// case t1: Call <()> print t1
|
||||
// case t1: Call <()> print t2
|
||||
// case t3: Call <()> print t3
|
||||
// default: Return
|
||||
// }
|
||||
if x == 1 {
|
||||
print(1)
|
||||
} else if x == 1 { // duplicate => unreachable
|
||||
print("1a")
|
||||
} else if x == 2 {
|
||||
print(2)
|
||||
}
|
||||
}
|
||||
|
||||
// Interface values (created by comparisons) are not constants,
|
||||
// so ConstSwitch.X is never of interface type.
|
||||
func MakeInterfaceIsNotAConstant(x interface{}) {
|
||||
if x == "foo" {
|
||||
print("foo")
|
||||
} else if x == 1 {
|
||||
print(1)
|
||||
}
|
||||
}
|
||||
|
||||
func ZeroInitializedVarsAreConstants(x int) {
|
||||
// switch t5 {
|
||||
// case t4: Call <()> print t1
|
||||
// case t2: Call <()> print t2
|
||||
// default: Call <()> print t3
|
||||
// }
|
||||
var zero int // SSA construction replaces zero with 0
|
||||
if x == zero {
|
||||
print(1)
|
||||
} else if x == 2 {
|
||||
print(2)
|
||||
}
|
||||
print("end")
|
||||
}
|
||||
|
||||
// -------- Type switches --------
|
||||
|
||||
// NB, potentially fragile reliance on register number.
|
||||
func AdHocTypeSwitch(x interface{}) {
|
||||
// switch t2.(type) {
|
||||
// case t4 int: Call <()> println t8
|
||||
// case t13 string: Call <()> println t16
|
||||
// default: Call <()> print t1
|
||||
// }
|
||||
if i, ok := x.(int); ok {
|
||||
println(i)
|
||||
} else if s, ok := x.(string); ok {
|
||||
println(s)
|
||||
} else {
|
||||
print("default")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package irutil
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
func Reachable(from, to *ir.BasicBlock) bool {
|
||||
if from == to {
|
||||
return true
|
||||
}
|
||||
if from.Dominates(to) {
|
||||
return true
|
||||
}
|
||||
|
||||
found := false
|
||||
Walk(from, func(b *ir.BasicBlock) bool {
|
||||
if b == to {
|
||||
found = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
func Walk(b *ir.BasicBlock, fn func(*ir.BasicBlock) bool) {
|
||||
seen := map[*ir.BasicBlock]bool{}
|
||||
wl := []*ir.BasicBlock{b}
|
||||
for len(wl) > 0 {
|
||||
b := wl[len(wl)-1]
|
||||
wl = wl[:len(wl)-1]
|
||||
if seen[b] {
|
||||
continue
|
||||
}
|
||||
seen[b] = true
|
||||
if !fn(b) {
|
||||
continue
|
||||
}
|
||||
wl = append(wl, b.Succs...)
|
||||
}
|
||||
}
|
||||
|
||||
func Vararg(x *ir.Slice) ([]ir.Value, bool) {
|
||||
var out []ir.Value
|
||||
alloc, ok := ir.Unwrap(x.X).(*ir.Alloc)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
var checkAlloc func(alloc ir.Value) bool
|
||||
checkAlloc = func(alloc ir.Value) bool {
|
||||
for _, ref := range *alloc.Referrers() {
|
||||
if ref == x {
|
||||
continue
|
||||
}
|
||||
if ref.Block() != x.Block() {
|
||||
return false
|
||||
}
|
||||
switch ref := ref.(type) {
|
||||
case *ir.IndexAddr:
|
||||
idx := ref
|
||||
if len(*idx.Referrers()) != 1 {
|
||||
return false
|
||||
}
|
||||
store, ok := (*idx.Referrers())[0].(*ir.Store)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
out = append(out, store.Val)
|
||||
case *ir.Copy:
|
||||
if !checkAlloc(ref) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !checkAlloc(alloc) {
|
||||
return nil, false
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func CallName(call *ir.CallCommon) string {
|
||||
if call.IsInvoke() {
|
||||
return ""
|
||||
}
|
||||
switch v := call.Value.(type) {
|
||||
case *ir.Function:
|
||||
fn, ok := v.Object().(*types.Func)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return typeutil.FuncName(fn)
|
||||
case *ir.Builtin:
|
||||
return v.Name()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func IsCallTo(call *ir.CallCommon, name string) bool { return CallName(call) == name }
|
||||
|
||||
func IsCallToAny(call *ir.CallCommon, names ...string) bool {
|
||||
q := CallName(call)
|
||||
for _, name := range names {
|
||||
if q == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func FilterDebug(instr []ir.Instruction) []ir.Instruction {
|
||||
var out []ir.Instruction
|
||||
for _, ins := range instr {
|
||||
if _, ok := ins.(*ir.DebugRef); !ok {
|
||||
out = append(out, ins)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func IsExample(fn *ir.Function) bool {
|
||||
if !strings.HasPrefix(fn.Name(), "Example") {
|
||||
return false
|
||||
}
|
||||
f := fn.Prog.Fset.File(fn.Pos())
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(f.Name(), "_test.go")
|
||||
}
|
||||
|
||||
// Flatten recursively returns the underlying value of an ir.Sigma or
|
||||
// ir.Phi node. If all edges in an ir.Phi node are the same (after
|
||||
// flattening), the flattened edge will get returned. If flattening is
|
||||
// not possible, nil is returned.
|
||||
func Flatten(v ir.Value) ir.Value {
|
||||
failed := false
|
||||
seen := map[ir.Value]struct{}{}
|
||||
var out ir.Value
|
||||
var dfs func(v ir.Value)
|
||||
dfs = func(v ir.Value) {
|
||||
if failed {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
return
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
|
||||
switch v := v.(type) {
|
||||
case *ir.Sigma:
|
||||
dfs(v.X)
|
||||
case *ir.Phi:
|
||||
for _, e := range v.Edges {
|
||||
dfs(e)
|
||||
}
|
||||
default:
|
||||
if out == nil {
|
||||
out = v
|
||||
} else if out != v {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
dfs(v)
|
||||
|
||||
if failed {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package irutil
|
||||
|
||||
import "honnef.co/go/tools/go/ir"
|
||||
|
||||
// This file defines utilities for visiting the IR of
|
||||
// a Program.
|
||||
//
|
||||
// TODO(adonovan): test coverage.
|
||||
|
||||
// AllFunctions finds and returns the set of functions potentially
|
||||
// needed by program prog, as determined by a simple linker-style
|
||||
// reachability algorithm starting from the members and method-sets of
|
||||
// each package. The result may include anonymous functions and
|
||||
// synthetic wrappers.
|
||||
//
|
||||
// Precondition: all packages are built.
|
||||
func AllFunctions(prog *ir.Program) map[*ir.Function]bool {
|
||||
visit := visitor{
|
||||
prog: prog,
|
||||
seen: make(map[*ir.Function]bool),
|
||||
}
|
||||
visit.program()
|
||||
return visit.seen
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
prog *ir.Program
|
||||
seen map[*ir.Function]bool
|
||||
}
|
||||
|
||||
func (visit *visitor) program() {
|
||||
for _, pkg := range visit.prog.AllPackages() {
|
||||
for _, mem := range pkg.Members {
|
||||
if fn, ok := mem.(*ir.Function); ok {
|
||||
visit.function(fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, T := range visit.prog.RuntimeTypes() {
|
||||
mset := visit.prog.MethodSets.MethodSet(T)
|
||||
for i, n := 0, mset.Len(); i < n; i++ {
|
||||
visit.function(visit.prog.MethodValue(mset.At(i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (visit *visitor) function(fn *ir.Function) {
|
||||
if !visit.seen[fn] {
|
||||
visit.seen[fn] = true
|
||||
var buf [10]*ir.Value // avoid alloc in common case
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
for _, op := range instr.Operands(buf[:0]) {
|
||||
if fn, ok := (*op).(*ir.Function); ok {
|
||||
visit.function(fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MainPackages returns the subset of the specified packages
|
||||
// named "main" that define a main function.
|
||||
// The result may include synthetic "testmain" packages.
|
||||
func MainPackages(pkgs []*ir.Package) []*ir.Package {
|
||||
var mains []*ir.Package
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Pkg.Name() == "main" && pkg.Func("main") != nil {
|
||||
mains = append(mains, pkg)
|
||||
}
|
||||
}
|
||||
return mains
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// lvalues are the union of addressable expressions and map-index
|
||||
// expressions.
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
// An lvalue represents an assignable location that may appear on the
|
||||
// left-hand side of an assignment. This is a generalization of a
|
||||
// pointer to permit updates to elements of maps.
|
||||
type lvalue interface {
|
||||
store(fn *Function, v Value, source ast.Node) // stores v into the location
|
||||
load(fn *Function, source ast.Node) Value // loads the contents of the location
|
||||
address(fn *Function) Value // address of the location
|
||||
typ() types.Type // returns the type of the location
|
||||
}
|
||||
|
||||
// An address is an lvalue represented by a true pointer.
|
||||
type address struct {
|
||||
addr Value
|
||||
expr ast.Expr // source syntax of the value (not address) [debug mode]
|
||||
}
|
||||
|
||||
func (a *address) load(fn *Function, source ast.Node) Value {
|
||||
return emitLoad(fn, a.addr, source)
|
||||
}
|
||||
|
||||
func (a *address) store(fn *Function, v Value, source ast.Node) {
|
||||
store := emitStore(fn, a.addr, v, source)
|
||||
if a.expr != nil {
|
||||
// store.Val is v, converted for assignability.
|
||||
emitDebugRef(fn, a.expr, store.Val, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *address) address(fn *Function) Value {
|
||||
if a.expr != nil {
|
||||
emitDebugRef(fn, a.expr, a.addr, true)
|
||||
}
|
||||
return a.addr
|
||||
}
|
||||
|
||||
func (a *address) typ() types.Type {
|
||||
return deref(a.addr.Type())
|
||||
}
|
||||
|
||||
type compositeElement struct {
|
||||
cv *CompositeValue
|
||||
idx int
|
||||
t types.Type
|
||||
expr ast.Expr
|
||||
}
|
||||
|
||||
func (ce *compositeElement) load(fn *Function, source ast.Node) Value {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (ce *compositeElement) store(fn *Function, v Value, source ast.Node) {
|
||||
v = emitConv(fn, v, ce.t, source)
|
||||
ce.cv.Values[ce.idx] = v
|
||||
if ce.expr != nil {
|
||||
// store.Val is v, converted for assignability.
|
||||
emitDebugRef(fn, ce.expr, v, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (ce *compositeElement) address(fn *Function) Value {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (ce *compositeElement) typ() types.Type {
|
||||
return ce.t
|
||||
}
|
||||
|
||||
// An element is an lvalue represented by m[k], the location of an
|
||||
// element of a map. These locations are not addressable
|
||||
// since pointers cannot be formed from them, but they do support
|
||||
// load() and store().
|
||||
type element struct {
|
||||
m, k Value // map
|
||||
t types.Type // map element type
|
||||
}
|
||||
|
||||
func (e *element) load(fn *Function, source ast.Node) Value {
|
||||
l := &MapLookup{
|
||||
X: e.m,
|
||||
Index: e.k,
|
||||
}
|
||||
l.setType(e.t)
|
||||
return fn.emit(l, source)
|
||||
}
|
||||
|
||||
func (e *element) store(fn *Function, v Value, source ast.Node) {
|
||||
up := &MapUpdate{
|
||||
Map: e.m,
|
||||
Key: e.k,
|
||||
Value: emitConv(fn, v, e.t, source),
|
||||
}
|
||||
fn.emit(up, source)
|
||||
}
|
||||
|
||||
func (e *element) address(fn *Function) Value {
|
||||
panic("map elements are not addressable")
|
||||
}
|
||||
|
||||
func (e *element) typ() types.Type {
|
||||
return e.t
|
||||
}
|
||||
|
||||
// A lazyAddress is an lvalue whose address is the result of an instruction.
|
||||
// These work like an *address except a new address.address() Value
|
||||
// is created on each load, store and address call.
|
||||
// A lazyAddress can be used to control when a side effect (nil pointer
|
||||
// dereference, index out of bounds) of using a location happens.
|
||||
type lazyAddress struct {
|
||||
addr func(fn *Function) Value // emit to fn the computation of the address
|
||||
t types.Type // type of the location
|
||||
expr ast.Expr // source syntax of the value (not address) [debug mode]
|
||||
}
|
||||
|
||||
func (l *lazyAddress) load(fn *Function, source ast.Node) Value {
|
||||
load := emitLoad(fn, l.addr(fn), source)
|
||||
return load
|
||||
}
|
||||
|
||||
func (l *lazyAddress) store(fn *Function, v Value, source ast.Node) {
|
||||
store := emitStore(fn, l.addr(fn), v, source)
|
||||
if l.expr != nil {
|
||||
// store.Val is v, converted for assignability.
|
||||
emitDebugRef(fn, l.expr, store.Val, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lazyAddress) address(fn *Function) Value {
|
||||
addr := l.addr(fn)
|
||||
if l.expr != nil {
|
||||
emitDebugRef(fn, l.expr, addr, true)
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func (l *lazyAddress) typ() types.Type { return l.t }
|
||||
|
||||
// A blank is a dummy variable whose name is "_".
|
||||
// It is not reified: loads are illegal and stores are ignored.
|
||||
type blank struct{}
|
||||
|
||||
func (bl blank) load(fn *Function, source ast.Node) Value {
|
||||
panic("blank.load is illegal")
|
||||
}
|
||||
|
||||
func (bl blank) store(fn *Function, v Value, source ast.Node) {
|
||||
s := &BlankStore{
|
||||
Val: v,
|
||||
}
|
||||
fn.emit(s, source)
|
||||
}
|
||||
|
||||
func (bl blank) address(fn *Function) Value {
|
||||
panic("blank var is not addressable")
|
||||
}
|
||||
|
||||
func (bl blank) typ() types.Type {
|
||||
// This should be the type of the blank Ident; the typechecker
|
||||
// doesn't provide this yet, but fortunately, we don't need it
|
||||
// yet either.
|
||||
panic("blank.typ is unimplemented")
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file defines utilities for population of method sets.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
|
||||
"honnef.co/go/tools/analysis/lint"
|
||||
)
|
||||
|
||||
// MethodValue returns the Function implementing method sel, building
|
||||
// wrapper methods on demand. It returns nil if sel denotes an
|
||||
// abstract (interface) method.
|
||||
//
|
||||
// Precondition: sel.Kind() == MethodVal.
|
||||
//
|
||||
// Thread-safe.
|
||||
//
|
||||
// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu)
|
||||
func (prog *Program) MethodValue(sel *types.Selection) *Function {
|
||||
if sel.Kind() != types.MethodVal {
|
||||
panic(fmt.Sprintf("MethodValue(%s) kind != MethodVal", sel))
|
||||
}
|
||||
T := sel.Recv()
|
||||
if isInterface(T) {
|
||||
return nil // abstract method
|
||||
}
|
||||
if prog.mode&LogSource != 0 {
|
||||
defer logStack("MethodValue %s %v", T, sel)()
|
||||
}
|
||||
|
||||
prog.methodsMu.Lock()
|
||||
defer prog.methodsMu.Unlock()
|
||||
|
||||
return prog.addMethod(prog.createMethodSet(T), sel)
|
||||
}
|
||||
|
||||
// LookupMethod returns the implementation of the method of type T
|
||||
// identified by (pkg, name). It returns nil if the method exists but
|
||||
// is abstract, and panics if T has no such method.
|
||||
func (prog *Program) LookupMethod(T types.Type, pkg *types.Package, name string) *Function {
|
||||
sel := prog.MethodSets.MethodSet(T).Lookup(pkg, name)
|
||||
if sel == nil {
|
||||
panic(fmt.Sprintf("%s has no method %s", T, types.Id(pkg, name)))
|
||||
}
|
||||
return prog.MethodValue(sel)
|
||||
}
|
||||
|
||||
// methodSet contains the (concrete) methods of a non-interface type.
|
||||
type methodSet struct {
|
||||
mapping map[string]*Function // populated lazily
|
||||
complete bool // mapping contains all methods
|
||||
}
|
||||
|
||||
// Precondition: !isInterface(T).
|
||||
// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu)
|
||||
func (prog *Program) createMethodSet(T types.Type) *methodSet {
|
||||
mset, ok := prog.methodSets.At(T)
|
||||
if !ok {
|
||||
mset = &methodSet{mapping: make(map[string]*Function)}
|
||||
prog.methodSets.Set(T, mset)
|
||||
}
|
||||
return mset
|
||||
}
|
||||
|
||||
// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu)
|
||||
func (prog *Program) addMethod(mset *methodSet, sel *types.Selection) *Function {
|
||||
if sel.Kind() == types.MethodExpr {
|
||||
panic(sel)
|
||||
}
|
||||
id := sel.Obj().Id()
|
||||
fn := mset.mapping[id]
|
||||
if fn == nil {
|
||||
obj := sel.Obj().(*types.Func)
|
||||
|
||||
needsPromotion := len(sel.Index()) > 1
|
||||
needsIndirection := !isPointer(recvType(obj)) && isPointer(sel.Recv())
|
||||
if needsPromotion || needsIndirection {
|
||||
fn = makeWrapper(prog, sel)
|
||||
} else {
|
||||
fn = prog.declaredFunc(obj)
|
||||
}
|
||||
if fn.Signature.Recv() == nil {
|
||||
panic(fn) // missing receiver
|
||||
}
|
||||
mset.mapping[id] = fn
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
// RuntimeTypes returns a new unordered slice containing all
|
||||
// concrete types in the program for which a complete (non-empty)
|
||||
// method set is required at run-time.
|
||||
//
|
||||
// Thread-safe.
|
||||
//
|
||||
// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu)
|
||||
func (prog *Program) RuntimeTypes() []types.Type {
|
||||
prog.methodsMu.Lock()
|
||||
defer prog.methodsMu.Unlock()
|
||||
|
||||
var res []types.Type
|
||||
prog.methodSets.Iterate(func(T types.Type, v *methodSet) {
|
||||
if v.complete {
|
||||
res = append(res, T)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// declaredFunc returns the concrete function/method denoted by obj.
|
||||
// Panic ensues if there is none.
|
||||
func (prog *Program) declaredFunc(obj *types.Func) *Function {
|
||||
if origin := obj.Origin(); origin != obj {
|
||||
// Calling method on instantiated type, create a wrapper that calls the generic type's method
|
||||
base := prog.packageLevelValue(origin)
|
||||
return makeInstance(prog, base.(*Function), obj.Type().(*types.Signature), nil)
|
||||
} else {
|
||||
if v := prog.packageLevelValue(obj); v != nil {
|
||||
return v.(*Function)
|
||||
}
|
||||
}
|
||||
panic("no concrete method: " + obj.String())
|
||||
}
|
||||
|
||||
// needMethodsOf ensures that runtime type information (including the
|
||||
// complete method set) is available for the specified type T and all
|
||||
// its subcomponents.
|
||||
//
|
||||
// needMethodsOf must be called for at least every type that is an
|
||||
// operand of some MakeInterface instruction, and for the type of
|
||||
// every exported package member.
|
||||
//
|
||||
// Precondition: T is not a method signature (*Signature with Recv()!=nil).
|
||||
//
|
||||
// Thread-safe. (Called via emitConv from multiple builder goroutines.)
|
||||
//
|
||||
// TODO(adonovan): make this faster. It accounts for 20% of SSA build time.
|
||||
//
|
||||
// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu)
|
||||
func (prog *Program) needMethodsOf(T types.Type) {
|
||||
prog.methodsMu.Lock()
|
||||
prog.needMethods(T, false)
|
||||
prog.methodsMu.Unlock()
|
||||
}
|
||||
|
||||
// Precondition: T is not a method signature (*Signature with Recv()!=nil).
|
||||
// Recursive case: skip => don't create methods for T.
|
||||
//
|
||||
// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu)
|
||||
func (prog *Program) needMethods(T types.Type, skip bool) {
|
||||
// Each package maintains its own set of types it has visited.
|
||||
if prevSkip, ok := prog.runtimeTypes.At(T); ok {
|
||||
// needMethods(T) was previously called
|
||||
if !prevSkip || skip {
|
||||
return // already seen, with same or false 'skip' value
|
||||
}
|
||||
}
|
||||
prog.runtimeTypes.Set(T, skip)
|
||||
|
||||
tmset := prog.MethodSets.MethodSet(T)
|
||||
|
||||
if !skip && !isInterface(T) && tmset.Len() > 0 {
|
||||
// Create methods of T.
|
||||
mset := prog.createMethodSet(T)
|
||||
if !mset.complete {
|
||||
mset.complete = true
|
||||
n := tmset.Len()
|
||||
for i := 0; i < n; i++ {
|
||||
prog.addMethod(mset, tmset.At(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursion over signatures of each method.
|
||||
for i := 0; i < tmset.Len(); i++ {
|
||||
sig := tmset.At(i).Type().(*types.Signature)
|
||||
prog.needMethods(sig.Params(), false)
|
||||
prog.needMethods(sig.Results(), false)
|
||||
}
|
||||
|
||||
switch t := T.(type) {
|
||||
case *types.Basic:
|
||||
// nop
|
||||
|
||||
case *types.Interface, *types.TypeParam:
|
||||
// nop---handled by recursion over method set.
|
||||
|
||||
case *types.Pointer:
|
||||
prog.needMethods(t.Elem(), false)
|
||||
|
||||
case *types.Slice:
|
||||
prog.needMethods(t.Elem(), false)
|
||||
|
||||
case *types.Chan:
|
||||
prog.needMethods(t.Elem(), false)
|
||||
|
||||
case *types.Map:
|
||||
prog.needMethods(t.Key(), false)
|
||||
prog.needMethods(t.Elem(), false)
|
||||
|
||||
case *types.Signature:
|
||||
if t.Recv() != nil {
|
||||
panic(fmt.Sprintf("Signature %s has Recv %s", t, t.Recv()))
|
||||
}
|
||||
prog.needMethods(t.Params(), false)
|
||||
prog.needMethods(t.Results(), false)
|
||||
|
||||
case *types.Named:
|
||||
// A pointer-to-named type can be derived from a named
|
||||
// type via reflection. It may have methods too.
|
||||
prog.needMethods(types.NewPointer(t), false)
|
||||
|
||||
// Consider 'type T struct{S}' where S has methods.
|
||||
// Reflection provides no way to get from T to struct{S},
|
||||
// only to S, so the method set of struct{S} is unwanted,
|
||||
// so set 'skip' flag during recursion.
|
||||
prog.needMethods(t.Underlying(), true)
|
||||
|
||||
case *types.Array:
|
||||
prog.needMethods(t.Elem(), false)
|
||||
|
||||
case *types.Struct:
|
||||
for i, n := 0, t.NumFields(); i < n; i++ {
|
||||
prog.needMethods(t.Field(i).Type(), false)
|
||||
}
|
||||
|
||||
case *types.Tuple:
|
||||
for i, n := 0, t.Len(); i < n; i++ {
|
||||
prog.needMethods(t.At(i).Type(), false)
|
||||
}
|
||||
|
||||
default:
|
||||
lint.ExhaustiveTypeSwitch(T)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file defines the BuilderMode type and its command-line flag.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// BuilderMode is a bitmask of options for diagnostics and checking.
|
||||
//
|
||||
// *BuilderMode satisfies the flag.Value interface. Example:
|
||||
//
|
||||
// var mode = ir.BuilderMode(0)
|
||||
// func init() { flag.Var(&mode, "build", ir.BuilderModeDoc) }
|
||||
type BuilderMode uint
|
||||
|
||||
const (
|
||||
PrintPackages BuilderMode = 1 << iota // Print package inventory to stdout
|
||||
PrintFunctions // Print function IR code to stdout
|
||||
PrintSource // Print source code when printing function IR
|
||||
LogSource // Log source locations as IR builder progresses
|
||||
SanityCheckFunctions // Perform sanity checking of function bodies
|
||||
NaiveForm // Build naïve IR form: don't replace local loads/stores with registers
|
||||
GlobalDebug // Enable debug info for all packages
|
||||
SplitAfterNewInformation // Split live range after we learn something new about a value
|
||||
)
|
||||
|
||||
const BuilderModeDoc = `Options controlling the IR builder.
|
||||
The value is a sequence of zero or more of these symbols:
|
||||
C perform sanity [C]hecking of the IR form.
|
||||
D include [D]ebug info for every function.
|
||||
P print [P]ackage inventory.
|
||||
F print [F]unction IR code.
|
||||
A print [A]ST nodes responsible for IR instructions
|
||||
S log [S]ource locations as IR builder progresses.
|
||||
N build [N]aive IR form: don't replace local loads/stores with registers.
|
||||
I Split live range after a value is used as slice or array index
|
||||
`
|
||||
|
||||
func (m BuilderMode) String() string {
|
||||
var buf bytes.Buffer
|
||||
if m&GlobalDebug != 0 {
|
||||
buf.WriteByte('D')
|
||||
}
|
||||
if m&PrintPackages != 0 {
|
||||
buf.WriteByte('P')
|
||||
}
|
||||
if m&PrintFunctions != 0 {
|
||||
buf.WriteByte('F')
|
||||
}
|
||||
if m&PrintSource != 0 {
|
||||
buf.WriteByte('A')
|
||||
}
|
||||
if m&LogSource != 0 {
|
||||
buf.WriteByte('S')
|
||||
}
|
||||
if m&SanityCheckFunctions != 0 {
|
||||
buf.WriteByte('C')
|
||||
}
|
||||
if m&NaiveForm != 0 {
|
||||
buf.WriteByte('N')
|
||||
}
|
||||
if m&SplitAfterNewInformation != 0 {
|
||||
buf.WriteByte('I')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Set parses the flag characters in s and updates *m.
|
||||
func (m *BuilderMode) Set(s string) error {
|
||||
var mode BuilderMode
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case 'D':
|
||||
mode |= GlobalDebug
|
||||
case 'P':
|
||||
mode |= PrintPackages
|
||||
case 'F':
|
||||
mode |= PrintFunctions
|
||||
case 'A':
|
||||
mode |= PrintSource
|
||||
case 'S':
|
||||
mode |= LogSource
|
||||
case 'C':
|
||||
mode |= SanityCheckFunctions
|
||||
case 'N':
|
||||
mode |= NaiveForm
|
||||
case 'I':
|
||||
mode |= SplitAfterNewInformation
|
||||
default:
|
||||
return fmt.Errorf("unknown BuilderMode option: %q", c)
|
||||
}
|
||||
}
|
||||
*m = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns m.
|
||||
func (m BuilderMode) Get() interface{} { return m }
|
||||
@@ -0,0 +1,507 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file implements the String() methods for all Value and
|
||||
// Instruction types.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// relName returns the name of v relative to i.
|
||||
// In most cases, this is identical to v.Name(), but references to
|
||||
// Functions (including methods) and Globals use RelString and
|
||||
// all types are displayed with relType, so that only cross-package
|
||||
// references are package-qualified.
|
||||
func relName(v Value, i Instruction) string {
|
||||
if v == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
var from *types.Package
|
||||
if i != nil {
|
||||
from = i.Parent().pkg()
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case Member: // *Function or *Global
|
||||
return v.RelString(from)
|
||||
}
|
||||
return v.Name()
|
||||
}
|
||||
|
||||
func relType(t types.Type, from *types.Package) string {
|
||||
return types.TypeString(t, types.RelativeTo(from))
|
||||
}
|
||||
|
||||
func relString(m Member, from *types.Package) string {
|
||||
// NB: not all globals have an Object (e.g. init$guard),
|
||||
// so use Package().Object not Object.Package().
|
||||
if pkg := m.Package().Pkg; pkg != nil && pkg != from {
|
||||
return fmt.Sprintf("%s.%s", pkg.Path(), m.Name())
|
||||
}
|
||||
return m.Name()
|
||||
}
|
||||
|
||||
// Value.String()
|
||||
//
|
||||
// This method is provided only for debugging.
|
||||
// It never appears in disassembly, which uses Value.Name().
|
||||
|
||||
func (v *Parameter) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("Parameter <%s> {%s}", relType(v.Type(), from), v.name)
|
||||
}
|
||||
|
||||
func (v *FreeVar) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("FreeVar <%s> %s", relType(v.Type(), from), v.Name())
|
||||
}
|
||||
|
||||
func (v *Builtin) String() string {
|
||||
return fmt.Sprintf("Builtin %s", v.Name())
|
||||
}
|
||||
|
||||
// Instruction.String()
|
||||
|
||||
func (v *Alloc) String() string {
|
||||
from := v.Parent().pkg()
|
||||
storage := "Stack"
|
||||
if v.Heap {
|
||||
storage = "Heap"
|
||||
}
|
||||
return fmt.Sprintf("%sAlloc <%s>", storage, relType(v.Type(), from))
|
||||
}
|
||||
|
||||
func (v *Sigma) String() string {
|
||||
from := v.Parent().pkg()
|
||||
s := fmt.Sprintf("Sigma <%s> [b%d] %s", relType(v.Type(), from), v.From.Index, v.X.Name())
|
||||
return s
|
||||
}
|
||||
|
||||
func (v *Phi) String() string {
|
||||
var b bytes.Buffer
|
||||
fmt.Fprintf(&b, "Phi <%s>", v.Type())
|
||||
for i, edge := range v.Edges {
|
||||
b.WriteString(" ")
|
||||
// Be robust against malformed CFG.
|
||||
if v.block == nil {
|
||||
b.WriteString("??")
|
||||
continue
|
||||
}
|
||||
block := -1
|
||||
if i < len(v.block.Preds) {
|
||||
block = v.block.Preds[i].Index
|
||||
}
|
||||
fmt.Fprintf(&b, "%d:", block)
|
||||
edgeVal := "<nil>" // be robust
|
||||
if edge != nil {
|
||||
edgeVal = relName(edge, v)
|
||||
}
|
||||
b.WriteString(edgeVal)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func printCall(v *CallCommon, prefix string, instr Instruction) string {
|
||||
var b bytes.Buffer
|
||||
if !v.IsInvoke() {
|
||||
if value, ok := instr.(Value); ok {
|
||||
fmt.Fprintf(&b, "%s <%s> %s", prefix, relType(value.Type(), instr.Parent().pkg()), relName(v.Value, instr))
|
||||
} else {
|
||||
fmt.Fprintf(&b, "%s %s", prefix, relName(v.Value, instr))
|
||||
}
|
||||
} else {
|
||||
if value, ok := instr.(Value); ok {
|
||||
fmt.Fprintf(&b, "%sInvoke <%s> %s.%s", prefix, relType(value.Type(), instr.Parent().pkg()), relName(v.Value, instr), v.Method.Name())
|
||||
} else {
|
||||
fmt.Fprintf(&b, "%sInvoke %s.%s", prefix, relName(v.Value, instr), v.Method.Name())
|
||||
}
|
||||
}
|
||||
for _, arg := range v.TypeArgs {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(relType(arg, instr.Parent().pkg()))
|
||||
}
|
||||
for _, arg := range v.Args {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(relName(arg, instr))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c *CallCommon) String() string {
|
||||
return printCall(c, "", nil)
|
||||
}
|
||||
|
||||
func (v *Call) String() string {
|
||||
return printCall(&v.Call, "Call", v)
|
||||
}
|
||||
|
||||
func (v *BinOp) String() string {
|
||||
return fmt.Sprintf("BinOp <%s> {%s} %s %s", relType(v.Type(), v.Parent().pkg()), v.Op.String(), relName(v.X, v), relName(v.Y, v))
|
||||
}
|
||||
|
||||
func (v *UnOp) String() string {
|
||||
return fmt.Sprintf("UnOp <%s> {%s} %s", relType(v.Type(), v.Parent().pkg()), v.Op.String(), relName(v.X, v))
|
||||
}
|
||||
|
||||
func (v *Load) String() string {
|
||||
return fmt.Sprintf("Load <%s> %s", relType(v.Type(), v.Parent().pkg()), relName(v.X, v))
|
||||
}
|
||||
|
||||
func (v *Copy) String() string {
|
||||
return fmt.Sprintf("Copy <%s> %s", relType(v.Type(), v.Parent().pkg()), relName(v.X, v))
|
||||
}
|
||||
|
||||
func printConv(prefix string, v, x Value) string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("%s <%s> %s",
|
||||
prefix,
|
||||
relType(v.Type(), from),
|
||||
relName(x, v.(Instruction)))
|
||||
}
|
||||
|
||||
func (v *ChangeType) String() string { return printConv("ChangeType", v, v.X) }
|
||||
func (v *Convert) String() string { return printConv("Convert", v, v.X) }
|
||||
func (v *ChangeInterface) String() string { return printConv("ChangeInterface", v, v.X) }
|
||||
func (v *SliceToArrayPointer) String() string { return printConv("SliceToArrayPointer", v, v.X) }
|
||||
func (v *SliceToArray) String() string { return printConv("SliceToArray", v, v.X) }
|
||||
func (v *MakeInterface) String() string { return printConv("MakeInterface", v, v.X) }
|
||||
|
||||
func (v *MakeClosure) String() string {
|
||||
from := v.Parent().pkg()
|
||||
var b bytes.Buffer
|
||||
fmt.Fprintf(&b, "MakeClosure <%s> %s", relType(v.Type(), from), relName(v.Fn, v))
|
||||
if v.Bindings != nil {
|
||||
for _, c := range v.Bindings {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(relName(c, v))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (v *MakeSlice) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("MakeSlice <%s> %s %s",
|
||||
relType(v.Type(), from),
|
||||
relName(v.Len, v),
|
||||
relName(v.Cap, v))
|
||||
}
|
||||
|
||||
func (v *Slice) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("Slice <%s> %s %s %s %s",
|
||||
relType(v.Type(), from), relName(v.X, v), relName(v.Low, v), relName(v.High, v), relName(v.Max, v))
|
||||
}
|
||||
|
||||
func (v *MakeMap) String() string {
|
||||
res := ""
|
||||
if v.Reserve != nil {
|
||||
res = relName(v.Reserve, v)
|
||||
}
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("MakeMap <%s> %s", relType(v.Type(), from), res)
|
||||
}
|
||||
|
||||
func (v *MakeChan) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("MakeChan <%s> %s", relType(v.Type(), from), relName(v.Size, v))
|
||||
}
|
||||
|
||||
func (v *FieldAddr) String() string {
|
||||
from := v.Parent().pkg()
|
||||
// v.X.Type() might be a pointer to a type parameter whose core type is a pointer to a struct
|
||||
st := deref(typeutil.CoreType(deref(v.X.Type()))).Underlying().(*types.Struct)
|
||||
// Be robust against a bad index.
|
||||
name := "?"
|
||||
if 0 <= v.Field && v.Field < st.NumFields() {
|
||||
name = st.Field(v.Field).Name()
|
||||
}
|
||||
return fmt.Sprintf("FieldAddr <%s> [%d] (%s) %s", relType(v.Type(), from), v.Field, name, relName(v.X, v))
|
||||
}
|
||||
|
||||
func (v *Field) String() string {
|
||||
st := typeutil.CoreType(v.X.Type()).Underlying().(*types.Struct)
|
||||
// Be robust against a bad index.
|
||||
name := "?"
|
||||
if 0 <= v.Field && v.Field < st.NumFields() {
|
||||
name = st.Field(v.Field).Name()
|
||||
}
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("Field <%s> [%d] (%s) %s", relType(v.Type(), from), v.Field, name, relName(v.X, v))
|
||||
}
|
||||
|
||||
func (v *IndexAddr) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("IndexAddr <%s> %s %s", relType(v.Type(), from), relName(v.X, v), relName(v.Index, v))
|
||||
}
|
||||
|
||||
func (v *Index) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("Index <%s> %s %s", relType(v.Type(), from), relName(v.X, v), relName(v.Index, v))
|
||||
}
|
||||
|
||||
func (v *MapLookup) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("MapLookup <%s> %s %s", relType(v.Type(), from), relName(v.X, v), relName(v.Index, v))
|
||||
}
|
||||
|
||||
func (v *StringLookup) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("StringLookup <%s> %s %s", relType(v.Type(), from), relName(v.X, v), relName(v.Index, v))
|
||||
}
|
||||
|
||||
func (v *Range) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("Range <%s> %s", relType(v.Type(), from), relName(v.X, v))
|
||||
}
|
||||
|
||||
func (v *Next) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("Next <%s> %s", relType(v.Type(), from), relName(v.Iter, v))
|
||||
}
|
||||
|
||||
func (v *TypeAssert) String() string {
|
||||
from := v.Parent().pkg()
|
||||
return fmt.Sprintf("TypeAssert <%s> %s", relType(v.Type(), from), relName(v.X, v))
|
||||
}
|
||||
|
||||
func (v *Extract) String() string {
|
||||
from := v.Parent().pkg()
|
||||
name := v.Tuple.Type().(*types.Tuple).At(v.Index).Name()
|
||||
return fmt.Sprintf("Extract <%s> [%d] (%s) %s", relType(v.Type(), from), v.Index, name, relName(v.Tuple, v))
|
||||
}
|
||||
|
||||
func (s *Jump) String() string {
|
||||
// Be robust against malformed CFG.
|
||||
block := -1
|
||||
if s.block != nil && len(s.block.Succs) == 1 {
|
||||
block = s.block.Succs[0].Index
|
||||
}
|
||||
str := fmt.Sprintf("Jump → b%d", block)
|
||||
if s.Comment() != "" {
|
||||
str = fmt.Sprintf("%s # %s", str, s.Comment())
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func (s *Unreachable) String() string {
|
||||
// Be robust against malformed CFG.
|
||||
block := -1
|
||||
if s.block != nil && len(s.block.Succs) == 1 {
|
||||
block = s.block.Succs[0].Index
|
||||
}
|
||||
return fmt.Sprintf("Unreachable → b%d", block)
|
||||
}
|
||||
|
||||
func (s *If) String() string {
|
||||
// Be robust against malformed CFG.
|
||||
tblock, fblock := -1, -1
|
||||
if s.block != nil && len(s.block.Succs) == 2 {
|
||||
tblock = s.block.Succs[0].Index
|
||||
fblock = s.block.Succs[1].Index
|
||||
}
|
||||
return fmt.Sprintf("If %s → b%d b%d", relName(s.Cond, s), tblock, fblock)
|
||||
}
|
||||
|
||||
func (s *ConstantSwitch) String() string {
|
||||
var b bytes.Buffer
|
||||
fmt.Fprintf(&b, "ConstantSwitch %s", relName(s.Tag, s))
|
||||
for _, cond := range s.Conds {
|
||||
fmt.Fprintf(&b, " %s", relName(cond, s))
|
||||
}
|
||||
fmt.Fprint(&b, " →")
|
||||
for _, succ := range s.block.Succs {
|
||||
fmt.Fprintf(&b, " b%d", succ.Index)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (v *CompositeValue) String() string {
|
||||
var b bytes.Buffer
|
||||
from := v.Parent().pkg()
|
||||
fmt.Fprintf(&b, "CompositeValue <%s>", relType(v.Type(), from))
|
||||
if v.NumSet >= len(v.Values) {
|
||||
// All values provided
|
||||
fmt.Fprint(&b, " [all]")
|
||||
} else if v.Bitmap.BitLen() == 0 {
|
||||
// No values provided
|
||||
fmt.Fprint(&b, " [none]")
|
||||
} else {
|
||||
// Some values provided
|
||||
bits := []byte(fmt.Sprintf("%0*b", len(v.Values), &v.Bitmap))
|
||||
for i := 0; i < len(bits)/2; i++ {
|
||||
o := len(bits) - 1 - i
|
||||
bits[i], bits[o] = bits[o], bits[i]
|
||||
}
|
||||
fmt.Fprintf(&b, " [%s]", bits)
|
||||
}
|
||||
for _, vv := range v.Values {
|
||||
fmt.Fprintf(&b, " %s", relName(vv, v))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (s *TypeSwitch) String() string {
|
||||
from := s.Parent().pkg()
|
||||
var b bytes.Buffer
|
||||
fmt.Fprintf(&b, "TypeSwitch <%s> %s", relType(s.typ, from), relName(s.Tag, s))
|
||||
for _, cond := range s.Conds {
|
||||
fmt.Fprintf(&b, " %q", relType(cond, s.block.parent.pkg()))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (s *Go) String() string {
|
||||
return printCall(&s.Call, "Go", s)
|
||||
}
|
||||
|
||||
func (s *Panic) String() string {
|
||||
// Be robust against malformed CFG.
|
||||
block := -1
|
||||
if s.block != nil && len(s.block.Succs) == 1 {
|
||||
block = s.block.Succs[0].Index
|
||||
}
|
||||
return fmt.Sprintf("Panic %s → b%d", relName(s.X, s), block)
|
||||
}
|
||||
|
||||
func (s *Return) String() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("Return")
|
||||
for _, r := range s.Results {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(relName(r, s))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (*RunDefers) String() string {
|
||||
return "RunDefers"
|
||||
}
|
||||
|
||||
func (s *Send) String() string {
|
||||
return fmt.Sprintf("Send %s %s", relName(s.Chan, s), relName(s.X, s))
|
||||
}
|
||||
|
||||
func (recv *Recv) String() string {
|
||||
from := recv.Parent().pkg()
|
||||
return fmt.Sprintf("Recv <%s> %s", relType(recv.Type(), from), relName(recv.Chan, recv))
|
||||
}
|
||||
|
||||
func (s *Defer) String() string {
|
||||
return printCall(&s.Call, "Defer", s)
|
||||
}
|
||||
|
||||
func (s *Select) String() string {
|
||||
var b bytes.Buffer
|
||||
for i, st := range s.States {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
if st.Dir == types.RecvOnly {
|
||||
b.WriteString("<-")
|
||||
b.WriteString(relName(st.Chan, s))
|
||||
} else {
|
||||
b.WriteString(relName(st.Chan, s))
|
||||
b.WriteString("<-")
|
||||
b.WriteString(relName(st.Send, s))
|
||||
}
|
||||
}
|
||||
non := ""
|
||||
if !s.Blocking {
|
||||
non = "Non"
|
||||
}
|
||||
from := s.Parent().pkg()
|
||||
return fmt.Sprintf("Select%sBlocking <%s> [%s]", non, relType(s.Type(), from), b.String())
|
||||
}
|
||||
|
||||
func (s *Store) String() string {
|
||||
return fmt.Sprintf("Store {%s} %s %s",
|
||||
s.Val.Type(), relName(s.Addr, s), relName(s.Val, s))
|
||||
}
|
||||
|
||||
func (s *BlankStore) String() string {
|
||||
return fmt.Sprintf("BlankStore %s", relName(s.Val, s))
|
||||
}
|
||||
|
||||
func (s *MapUpdate) String() string {
|
||||
return fmt.Sprintf("MapUpdate %s %s %s", relName(s.Map, s), relName(s.Key, s), relName(s.Value, s))
|
||||
}
|
||||
|
||||
func (s *DebugRef) String() string {
|
||||
p := s.Parent().Prog.Fset.Position(s.Pos())
|
||||
var descr interface{}
|
||||
if s.object != nil {
|
||||
descr = s.object // e.g. "var x int"
|
||||
} else {
|
||||
descr = reflect.TypeOf(s.Expr) // e.g. "*ast.CallExpr"
|
||||
}
|
||||
var addr string
|
||||
if s.IsAddr {
|
||||
addr = "address of "
|
||||
}
|
||||
return fmt.Sprintf("; %s%s @ %d:%d is %s", addr, descr, p.Line, p.Column, s.X.Name())
|
||||
}
|
||||
|
||||
func (p *Package) String() string {
|
||||
return "package " + p.Pkg.Path()
|
||||
}
|
||||
|
||||
var _ io.WriterTo = (*Package)(nil) // *Package implements io.Writer
|
||||
|
||||
func (p *Package) WriteTo(w io.Writer) (int64, error) {
|
||||
var buf bytes.Buffer
|
||||
WritePackage(&buf, p)
|
||||
n, err := w.Write(buf.Bytes())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// WritePackage writes to buf a human-readable summary of p.
|
||||
func WritePackage(buf *bytes.Buffer, p *Package) {
|
||||
fmt.Fprintf(buf, "%s:\n", p)
|
||||
|
||||
var names []string
|
||||
maxname := 0
|
||||
for name := range p.Members {
|
||||
if l := len(name); l > maxname {
|
||||
maxname = l
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
from := p.Pkg
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
switch mem := p.Members[name].(type) {
|
||||
case *NamedConst:
|
||||
fmt.Fprintf(buf, " const %-*s %s = %s\n",
|
||||
maxname, name, mem.Name(), mem.Value.RelString(from))
|
||||
|
||||
case *Function:
|
||||
fmt.Fprintf(buf, " func %-*s %s\n",
|
||||
maxname, name, relType(mem.Type(), from))
|
||||
|
||||
case *Type:
|
||||
fmt.Fprintf(buf, " type %-*s %s\n",
|
||||
maxname, name, relType(mem.Type().Underlying(), from))
|
||||
for _, meth := range typeutil.IntuitiveMethodSet(mem.Type(), &p.Prog.MethodSets) {
|
||||
fmt.Fprintf(buf, " %s\n", types.SelectionString(meth, types.RelativeTo(from)))
|
||||
}
|
||||
|
||||
case *Global:
|
||||
fmt.Fprintf(buf, " var %-*s %s\n",
|
||||
maxname, name, relType(mem.Type().(*types.Pointer).Elem(), from))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// An optional pass for sanity-checking invariants of the IR representation.
|
||||
// Currently it checks CFG invariants but little at the instruction level.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
type sanity struct {
|
||||
reporter io.Writer
|
||||
fn *Function
|
||||
block *BasicBlock
|
||||
instrs map[Instruction]struct{}
|
||||
insane bool
|
||||
}
|
||||
|
||||
// sanityCheck performs integrity checking of the IR representation
|
||||
// of the function fn and returns true if it was valid. Diagnostics
|
||||
// are written to reporter if non-nil, os.Stderr otherwise. Some
|
||||
// diagnostics are only warnings and do not imply a negative result.
|
||||
//
|
||||
// Sanity-checking is intended to facilitate the debugging of code
|
||||
// transformation passes.
|
||||
func sanityCheck(fn *Function, reporter io.Writer) bool {
|
||||
if reporter == nil {
|
||||
reporter = os.Stderr
|
||||
}
|
||||
return (&sanity{reporter: reporter}).checkFunction(fn)
|
||||
}
|
||||
|
||||
// mustSanityCheck is like sanityCheck but panics instead of returning
|
||||
// a negative result.
|
||||
func mustSanityCheck(fn *Function, reporter io.Writer) {
|
||||
if !sanityCheck(fn, reporter) {
|
||||
fn.WriteTo(os.Stderr)
|
||||
panic("SanityCheck failed")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sanity) diagnostic(prefix, format string, args ...interface{}) {
|
||||
fmt.Fprintf(s.reporter, "%s: function %s", prefix, s.fn)
|
||||
if s.block != nil {
|
||||
fmt.Fprintf(s.reporter, ", block %s", s.block)
|
||||
}
|
||||
io.WriteString(s.reporter, ": ")
|
||||
fmt.Fprintf(s.reporter, format, args...)
|
||||
io.WriteString(s.reporter, "\n")
|
||||
}
|
||||
|
||||
func (s *sanity) errorf(format string, args ...interface{}) {
|
||||
s.insane = true
|
||||
s.diagnostic("Error", format, args...)
|
||||
}
|
||||
|
||||
func (s *sanity) warnf(format string, args ...interface{}) {
|
||||
s.diagnostic("Warning", format, args...)
|
||||
}
|
||||
|
||||
// findDuplicate returns an arbitrary basic block that appeared more
|
||||
// than once in blocks, or nil if all were unique.
|
||||
func findDuplicate(blocks []*BasicBlock) *BasicBlock {
|
||||
if len(blocks) < 2 {
|
||||
return nil
|
||||
}
|
||||
if blocks[0] == blocks[1] {
|
||||
return blocks[0]
|
||||
}
|
||||
// Slow path:
|
||||
m := make(map[*BasicBlock]bool)
|
||||
for _, b := range blocks {
|
||||
if m[b] {
|
||||
return b
|
||||
}
|
||||
m[b] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sanity) checkInstr(idx int, instr Instruction) {
|
||||
switch instr := instr.(type) {
|
||||
case *If, *Jump, *Return, *Panic, *Unreachable, *ConstantSwitch:
|
||||
s.errorf("control flow instruction not at end of block")
|
||||
case *Sigma:
|
||||
if idx > 0 {
|
||||
prev := s.block.Instrs[idx-1]
|
||||
if _, ok := prev.(*Sigma); !ok {
|
||||
s.errorf("Sigma instruction follows a non-Sigma: %T", prev)
|
||||
}
|
||||
}
|
||||
case *Phi:
|
||||
if idx == 0 {
|
||||
// It suffices to apply this check to just the first phi node.
|
||||
if dup := findDuplicate(s.block.Preds); dup != nil {
|
||||
s.errorf("phi node in block with duplicate predecessor %s", dup)
|
||||
}
|
||||
} else {
|
||||
prev := s.block.Instrs[idx-1]
|
||||
switch prev.(type) {
|
||||
case *Phi, *Sigma:
|
||||
default:
|
||||
s.errorf("Phi instruction follows a non-Phi, non-Sigma: %T", prev)
|
||||
}
|
||||
}
|
||||
if ne, np := len(instr.Edges), len(s.block.Preds); ne != np {
|
||||
s.errorf("phi node has %d edges but %d predecessors", ne, np)
|
||||
|
||||
} else {
|
||||
for i, e := range instr.Edges {
|
||||
if e == nil {
|
||||
s.errorf("phi node '%v' has no value for edge #%d from %s", instr, i, s.block.Preds[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *Alloc:
|
||||
if !instr.Heap {
|
||||
found := false
|
||||
for _, l := range s.fn.Locals {
|
||||
if l == instr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
s.errorf("local alloc %s = %s does not appear in Function.Locals", instr.Name(), instr)
|
||||
}
|
||||
}
|
||||
|
||||
case *BinOp:
|
||||
case *Call:
|
||||
case *ChangeInterface:
|
||||
case *ChangeType:
|
||||
case *SliceToArrayPointer:
|
||||
case *SliceToArray:
|
||||
case *Convert:
|
||||
tsetInstrX := typeutil.NewTypeSet(instr.X.Type().Underlying())
|
||||
tsetInstr := typeutil.NewTypeSet(instr.Type().Underlying())
|
||||
ok1 := tsetInstr.Any(func(term *types.Term) bool { _, ok := term.Type().Underlying().(*types.Basic); return ok })
|
||||
ok2 := tsetInstrX.Any(func(term *types.Term) bool { _, ok := term.Type().Underlying().(*types.Basic); return ok })
|
||||
if !ok1 && !ok2 {
|
||||
s.errorf("convert %s -> %s: at least one type set must contain basic type", instr.X.Type(), instr.Type())
|
||||
}
|
||||
|
||||
case *Defer:
|
||||
case *Extract:
|
||||
case *Field:
|
||||
case *FieldAddr:
|
||||
case *Go:
|
||||
case *Index:
|
||||
case *IndexAddr:
|
||||
case *MapLookup:
|
||||
case *StringLookup:
|
||||
case *MakeChan:
|
||||
case *MakeClosure:
|
||||
numFree := len(instr.Fn.(*Function).FreeVars)
|
||||
numBind := len(instr.Bindings)
|
||||
if numFree != numBind {
|
||||
s.errorf("MakeClosure has %d Bindings for function %s with %d free vars",
|
||||
numBind, instr.Fn, numFree)
|
||||
|
||||
}
|
||||
if recv := instr.Type().(*types.Signature).Recv(); recv != nil {
|
||||
s.errorf("MakeClosure's type includes receiver %s", recv.Type())
|
||||
}
|
||||
|
||||
case *MakeInterface:
|
||||
case *MakeMap:
|
||||
case *MakeSlice:
|
||||
case *MapUpdate:
|
||||
case *Next:
|
||||
case *Range:
|
||||
case *RunDefers:
|
||||
case *Select:
|
||||
case *Send:
|
||||
case *Slice:
|
||||
case *Store:
|
||||
case *TypeAssert:
|
||||
case *UnOp:
|
||||
case *DebugRef:
|
||||
case *BlankStore:
|
||||
case *Load:
|
||||
case *Parameter:
|
||||
case *Const:
|
||||
case *AggregateConst:
|
||||
case *ArrayConst:
|
||||
case *GenericConst:
|
||||
case *Recv:
|
||||
case *TypeSwitch:
|
||||
case *CompositeValue:
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown instruction type: %T", instr))
|
||||
}
|
||||
|
||||
if call, ok := instr.(CallInstruction); ok {
|
||||
if call.Common().Signature() == nil {
|
||||
s.errorf("nil signature: %s", call)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that value-defining instructions have valid types
|
||||
// and a valid referrer list.
|
||||
if v, ok := instr.(Value); ok {
|
||||
t := v.Type()
|
||||
if t == nil {
|
||||
s.errorf("no type: %s = %s", v.Name(), v)
|
||||
} else if b, ok := t.Underlying().(*types.Basic); ok && b.Info()&types.IsUntyped != 0 {
|
||||
if _, ok := v.(*Const); !ok {
|
||||
s.errorf("instruction has 'untyped' result: %s = %s : %s", v.Name(), v, t)
|
||||
}
|
||||
}
|
||||
s.checkReferrerList(v)
|
||||
}
|
||||
|
||||
// Untyped constants are legal as instruction Operands(),
|
||||
// for example:
|
||||
// _ = "foo"[0]
|
||||
// or:
|
||||
// if wordsize==64 {...}
|
||||
|
||||
// All other non-Instruction Values can be found via their
|
||||
// enclosing Function or Package.
|
||||
}
|
||||
|
||||
func (s *sanity) checkFinalInstr(instr Instruction) {
|
||||
switch instr := instr.(type) {
|
||||
case *If:
|
||||
if nsuccs := len(s.block.Succs); nsuccs != 2 {
|
||||
s.errorf("If-terminated block has %d successors; expected 2", nsuccs)
|
||||
return
|
||||
}
|
||||
if s.block.Succs[0] == s.block.Succs[1] {
|
||||
s.errorf("If-instruction has same True, False target blocks: %s", s.block.Succs[0])
|
||||
return
|
||||
}
|
||||
|
||||
case *Jump:
|
||||
if nsuccs := len(s.block.Succs); nsuccs != 1 {
|
||||
s.errorf("Jump-terminated block has %d successors; expected 1", nsuccs)
|
||||
return
|
||||
}
|
||||
|
||||
case *Return:
|
||||
if nsuccs := len(s.block.Succs); nsuccs != 0 {
|
||||
s.errorf("Return-terminated block has %d successors; expected none", nsuccs)
|
||||
return
|
||||
}
|
||||
if na, nf := len(instr.Results), s.fn.Signature.Results().Len(); nf != na {
|
||||
s.errorf("%d-ary return in %d-ary function", na, nf)
|
||||
}
|
||||
|
||||
case *Panic:
|
||||
if nsuccs := len(s.block.Succs); nsuccs != 1 {
|
||||
s.errorf("Panic-terminated block has %d successors; expected one", nsuccs)
|
||||
return
|
||||
}
|
||||
|
||||
case *Unreachable:
|
||||
if nsuccs := len(s.block.Succs); nsuccs != 1 {
|
||||
s.errorf("Unreachable-terminated block has %d successors; expected one", nsuccs)
|
||||
return
|
||||
}
|
||||
|
||||
case *ConstantSwitch:
|
||||
|
||||
default:
|
||||
s.errorf("non-control flow instruction at end of block")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sanity) checkBlock(b *BasicBlock, index int) {
|
||||
s.block = b
|
||||
|
||||
if b.Index != index {
|
||||
s.errorf("block has incorrect Index %d", b.Index)
|
||||
}
|
||||
if b.parent != s.fn {
|
||||
s.errorf("block has incorrect parent %s", b.parent)
|
||||
}
|
||||
|
||||
// Check all blocks are reachable.
|
||||
// (The entry block is always implicitly reachable, the exit block may be unreachable.)
|
||||
if index > 1 && len(b.Preds) == 0 {
|
||||
s.warnf("unreachable block")
|
||||
if b.Instrs == nil {
|
||||
// Since this block is about to be pruned,
|
||||
// tolerating transient problems in it
|
||||
// simplifies other optimizations.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check predecessor and successor relations are dual,
|
||||
// and that all blocks in CFG belong to same function.
|
||||
for _, a := range b.Preds {
|
||||
found := false
|
||||
for _, bb := range a.Succs {
|
||||
if bb == b {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
s.errorf("expected successor edge in predecessor %s; found only: %s", a, a.Succs)
|
||||
}
|
||||
if a.parent != s.fn {
|
||||
s.errorf("predecessor %s belongs to different function %s", a, a.parent)
|
||||
}
|
||||
}
|
||||
for _, c := range b.Succs {
|
||||
found := false
|
||||
for _, bb := range c.Preds {
|
||||
if bb == b {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
s.errorf("expected predecessor edge in successor %s; found only: %s", c, c.Preds)
|
||||
}
|
||||
if c.parent != s.fn {
|
||||
s.errorf("successor %s belongs to different function %s", c, c.parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Check each instruction is sane.
|
||||
n := len(b.Instrs)
|
||||
if n == 0 {
|
||||
s.errorf("basic block contains no instructions")
|
||||
}
|
||||
var rands [10]*Value // reuse storage
|
||||
for j, instr := range b.Instrs {
|
||||
if instr == nil {
|
||||
s.errorf("nil instruction at index %d", j)
|
||||
continue
|
||||
}
|
||||
if b2 := instr.Block(); b2 == nil {
|
||||
s.errorf("nil Block() for instruction at index %d", j)
|
||||
continue
|
||||
} else if b2 != b {
|
||||
s.errorf("wrong Block() (%s) for instruction at index %d ", b2, j)
|
||||
continue
|
||||
}
|
||||
if j < n-1 {
|
||||
s.checkInstr(j, instr)
|
||||
} else {
|
||||
s.checkFinalInstr(instr)
|
||||
}
|
||||
|
||||
// Check Instruction.Operands.
|
||||
operands:
|
||||
for i, op := range instr.Operands(rands[:0]) {
|
||||
if op == nil {
|
||||
s.errorf("nil operand pointer %d of %s", i, instr)
|
||||
continue
|
||||
}
|
||||
val := *op
|
||||
if val == nil {
|
||||
continue // a nil operand is ok
|
||||
}
|
||||
|
||||
// Check that "untyped" types only appear on constant operands.
|
||||
if _, ok := (*op).(*Const); !ok {
|
||||
if basic, ok := (*op).Type().(*types.Basic); ok {
|
||||
if basic.Info()&types.IsUntyped != 0 {
|
||||
s.errorf("operand #%d of %s is untyped: %s", i, instr, basic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that Operands that are also Instructions belong to same function.
|
||||
// TODO(adonovan): also check their block dominates block b.
|
||||
if val, ok := val.(Instruction); ok {
|
||||
if val.Block() == nil {
|
||||
s.errorf("operand %d of %s is an instruction (%s) that belongs to no block", i, instr, val)
|
||||
} else if val.Parent() != s.fn {
|
||||
s.errorf("operand %d of %s is an instruction (%s) from function %s", i, instr, val, val.Parent())
|
||||
}
|
||||
}
|
||||
|
||||
// Check that each function-local operand of
|
||||
// instr refers back to instr. (NB: quadratic)
|
||||
switch val := val.(type) {
|
||||
case *Const, *Global, *Builtin:
|
||||
continue // not local
|
||||
case *Function:
|
||||
if val.parent == nil {
|
||||
continue // only anon functions are local
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adonovan): check val.Parent() != nil <=> val.Referrers() is defined.
|
||||
|
||||
if refs := val.Referrers(); refs != nil {
|
||||
for _, ref := range *refs {
|
||||
if ref == instr {
|
||||
continue operands
|
||||
}
|
||||
}
|
||||
s.errorf("operand %d of %s (%s) does not refer to us", i, instr, val)
|
||||
} else {
|
||||
s.errorf("operand %d of %s (%s) has no referrers", i, instr, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sanity) checkReferrerList(v Value) {
|
||||
refs := v.Referrers()
|
||||
if refs == nil {
|
||||
s.errorf("%s has missing referrer list", v.Name())
|
||||
return
|
||||
}
|
||||
for i, ref := range *refs {
|
||||
if _, ok := s.instrs[ref]; !ok {
|
||||
if val, ok := ref.(Value); ok {
|
||||
s.errorf("%s.Referrers()[%d] = %s = %s is not an instruction belonging to this function", v.Name(), i, val.Name(), val)
|
||||
} else {
|
||||
s.errorf("%s.Referrers()[%d] = %s is not an instruction belonging to this function", v.Name(), i, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sanity) checkFunction(fn *Function) bool {
|
||||
// TODO(adonovan): check Function invariants:
|
||||
// - check params match signature
|
||||
// - check transient fields are nil
|
||||
// - warn if any fn.Locals do not appear among block instructions.
|
||||
s.fn = fn
|
||||
if fn.Prog == nil {
|
||||
s.errorf("nil Prog")
|
||||
}
|
||||
|
||||
_ = fn.String() // must not crash
|
||||
_ = fn.RelString(fn.pkg()) // must not crash
|
||||
|
||||
// All functions have a package, except delegates (which are
|
||||
// shared across packages, or duplicated as weak symbols in a
|
||||
// separate-compilation model), and error.Error.
|
||||
if fn.Pkg == nil {
|
||||
switch fn.Synthetic {
|
||||
case SyntheticWrapper, SyntheticBound, SyntheticThunk, SyntheticGeneric:
|
||||
default:
|
||||
if !strings.HasSuffix(fn.name, "Error") {
|
||||
s.errorf("nil Pkg")
|
||||
}
|
||||
}
|
||||
}
|
||||
if src, syn := fn.Synthetic == 0, fn.source != nil; src != syn {
|
||||
s.errorf("got fromSource=%t, hasSyntax=%t; want same values", src, syn)
|
||||
}
|
||||
for i, l := range fn.Locals {
|
||||
if l.Parent() != fn {
|
||||
s.errorf("Local %s at index %d has wrong parent", l.Name(), i)
|
||||
}
|
||||
if l.Heap {
|
||||
s.errorf("Local %s at index %d has Heap flag set", l.Name(), i)
|
||||
}
|
||||
}
|
||||
// Build the set of valid referrers.
|
||||
s.instrs = make(map[Instruction]struct{})
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
s.instrs[instr] = struct{}{}
|
||||
}
|
||||
}
|
||||
for i, p := range fn.Params {
|
||||
if p.Parent() != fn {
|
||||
s.errorf("Param %s at index %d has wrong parent", p.Name(), i)
|
||||
}
|
||||
// Check common suffix of Signature and Params match type.
|
||||
if sig := fn.Signature; sig != nil {
|
||||
j := i - len(fn.Params) + sig.Params().Len() // index within sig.Params
|
||||
if j < 0 {
|
||||
continue
|
||||
}
|
||||
if !types.Identical(p.Type(), sig.Params().At(j).Type()) {
|
||||
s.errorf("Param %s at index %d has wrong type (%s, versus %s in Signature)", p.Name(), i, p.Type(), sig.Params().At(j).Type())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
s.checkReferrerList(p)
|
||||
}
|
||||
for i, fv := range fn.FreeVars {
|
||||
if fv.Parent() != fn {
|
||||
s.errorf("FreeVar %s at index %d has wrong parent", fv.Name(), i)
|
||||
}
|
||||
s.checkReferrerList(fv)
|
||||
}
|
||||
|
||||
if fn.Blocks != nil && len(fn.Blocks) == 0 {
|
||||
// Function _had_ blocks (so it's not external) but
|
||||
// they were "optimized" away, even the entry block.
|
||||
s.errorf("Blocks slice is non-nil but empty")
|
||||
}
|
||||
for i, b := range fn.Blocks {
|
||||
if b == nil {
|
||||
s.warnf("nil *BasicBlock at f.Blocks[%d]", i)
|
||||
continue
|
||||
}
|
||||
s.checkBlock(b, i)
|
||||
}
|
||||
|
||||
s.block = nil
|
||||
for i, anon := range fn.AnonFuncs {
|
||||
if anon.Parent() != fn {
|
||||
s.errorf("AnonFuncs[%d]=%s but %s.Parent()=%s", i, anon, anon, anon.Parent())
|
||||
}
|
||||
}
|
||||
s.fn = nil
|
||||
return !s.insane
|
||||
}
|
||||
|
||||
// sanityCheckPackage checks invariants of packages upon creation.
|
||||
// It does not require that the package is built.
|
||||
// Unlike sanityCheck (for functions), it just panics at the first error.
|
||||
func sanityCheckPackage(pkg *Package) {
|
||||
if pkg.Pkg == nil {
|
||||
panic(fmt.Sprintf("Package %s has no Object", pkg))
|
||||
}
|
||||
_ = pkg.String() // must not crash
|
||||
|
||||
for name, mem := range pkg.Members {
|
||||
if name != mem.Name() {
|
||||
panic(fmt.Sprintf("%s: %T.Name() = %s, want %s",
|
||||
pkg.Pkg.Path(), mem, mem.Name(), name))
|
||||
}
|
||||
obj := mem.Object()
|
||||
if obj == nil {
|
||||
// This check is sound because fields
|
||||
// {Global,Function}.object have type
|
||||
// types.Object. (If they were declared as
|
||||
// *types.{Var,Func}, we'd have a non-empty
|
||||
// interface containing a nil pointer.)
|
||||
|
||||
continue // not all members have typechecker objects
|
||||
}
|
||||
if obj.Name() != name {
|
||||
if obj.Name() == "init" && strings.HasPrefix(mem.Name(), "init#") {
|
||||
// Ok. The name of a declared init function varies between
|
||||
// its types.Func ("init") and its ir.Function ("init#%d").
|
||||
} else {
|
||||
panic(fmt.Sprintf("%s: %T.Object().Name() = %s, want %s",
|
||||
pkg.Pkg.Path(), mem, obj.Name(), name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ir
|
||||
|
||||
// This file defines utilities for working with source positions
|
||||
// or source-level named entities ("objects").
|
||||
|
||||
// TODO(adonovan): test that {Value,Instruction}.Pos() positions match
|
||||
// the originating syntax, as specified.
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
// EnclosingFunction returns the function that contains the syntax
|
||||
// node denoted by path.
|
||||
//
|
||||
// Syntax associated with package-level variable specifications is
|
||||
// enclosed by the package's init() function.
|
||||
//
|
||||
// Returns nil if not found; reasons might include:
|
||||
// - the node is not enclosed by any function.
|
||||
// - the node is within an anonymous function (FuncLit) and
|
||||
// its IR function has not been created yet
|
||||
// (pkg.Build() has not yet been called).
|
||||
func EnclosingFunction(pkg *Package, path []ast.Node) *Function {
|
||||
// Start with package-level function...
|
||||
fn := findEnclosingPackageLevelFunction(pkg, path)
|
||||
if fn == nil {
|
||||
return nil // not in any function
|
||||
}
|
||||
|
||||
// ...then walk down the nested anonymous functions.
|
||||
n := len(path)
|
||||
outer:
|
||||
for i := range path {
|
||||
if lit, ok := path[n-1-i].(*ast.FuncLit); ok {
|
||||
for _, anon := range fn.AnonFuncs {
|
||||
if anon.Pos() == lit.Type.Func {
|
||||
fn = anon
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
// IR function not found:
|
||||
// - package not yet built, or maybe
|
||||
// - builder skipped FuncLit in dead block
|
||||
// (in principle; but currently the Builder
|
||||
// generates even dead FuncLits).
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
// HasEnclosingFunction returns true if the AST node denoted by path
|
||||
// is contained within the declaration of some function or
|
||||
// package-level variable.
|
||||
//
|
||||
// Unlike EnclosingFunction, the behaviour of this function does not
|
||||
// depend on whether IR code for pkg has been built, so it can be
|
||||
// used to quickly reject check inputs that will cause
|
||||
// EnclosingFunction to fail, prior to IR building.
|
||||
func HasEnclosingFunction(pkg *Package, path []ast.Node) bool {
|
||||
return findEnclosingPackageLevelFunction(pkg, path) != nil
|
||||
}
|
||||
|
||||
// findEnclosingPackageLevelFunction returns the Function
|
||||
// corresponding to the package-level function enclosing path.
|
||||
func findEnclosingPackageLevelFunction(pkg *Package, path []ast.Node) *Function {
|
||||
if n := len(path); n >= 2 { // [... {Gen,Func}Decl File]
|
||||
switch decl := path[n-2].(type) {
|
||||
case *ast.GenDecl:
|
||||
if decl.Tok == token.VAR && n >= 3 {
|
||||
// Package-level 'var' initializer.
|
||||
return pkg.init
|
||||
}
|
||||
|
||||
case *ast.FuncDecl:
|
||||
// Declared function/method.
|
||||
fn := findNamedFunc(pkg, decl.Pos())
|
||||
if fn == nil && decl.Recv == nil && decl.Name.Name == "init" {
|
||||
// Hack: return non-nil when IR is not yet
|
||||
// built so that HasEnclosingFunction works.
|
||||
return pkg.init
|
||||
}
|
||||
return fn
|
||||
}
|
||||
}
|
||||
return nil // not in any function
|
||||
}
|
||||
|
||||
// findNamedFunc returns the named function whose FuncDecl.Ident is at
|
||||
// position pos.
|
||||
func findNamedFunc(pkg *Package, pos token.Pos) *Function {
|
||||
for _, fn := range pkg.Functions {
|
||||
if fn.Pos() == pos {
|
||||
return fn
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValueForExpr returns the IR Value that corresponds to non-constant
|
||||
// expression e.
|
||||
//
|
||||
// It returns nil if no value was found, e.g.
|
||||
// - the expression is not lexically contained within f;
|
||||
// - f was not built with debug information; or
|
||||
// - e is a constant expression. (For efficiency, no debug
|
||||
// information is stored for constants. Use
|
||||
// go/types.Info.Types[e].Value instead.)
|
||||
// - e is a reference to nil or a built-in function.
|
||||
// - the value was optimised away.
|
||||
//
|
||||
// If e is an addressable expression used in an lvalue context,
|
||||
// value is the address denoted by e, and isAddr is true.
|
||||
//
|
||||
// The types of e (or &e, if isAddr) and the result are equal
|
||||
// (modulo "untyped" bools resulting from comparisons).
|
||||
//
|
||||
// (Tip: to find the ir.Value given a source position, use
|
||||
// astutil.PathEnclosingInterval to locate the ast.Node, then
|
||||
// EnclosingFunction to locate the Function, then ValueForExpr to find
|
||||
// the ir.Value.)
|
||||
func (f *Function) ValueForExpr(e ast.Expr) (value Value, isAddr bool) {
|
||||
if f.debugInfo() { // (opt)
|
||||
e = unparen(e)
|
||||
for _, b := range f.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
if ref, ok := instr.(*DebugRef); ok {
|
||||
if ref.Expr == e {
|
||||
return ref.X, ref.IsAddr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// --- Lookup functions for source-level named entities (types.Objects) ---
|
||||
|
||||
// Package returns the IR Package corresponding to the specified
|
||||
// type-checker package object.
|
||||
// It returns nil if no such IR package has been created.
|
||||
func (prog *Program) Package(obj *types.Package) *Package {
|
||||
return prog.packages[obj]
|
||||
}
|
||||
|
||||
// packageLevelValue returns the package-level value corresponding to
|
||||
// the specified named object, which may be a package-level const
|
||||
// (*Const), var (*Global) or func (*Function) of some package in
|
||||
// prog. It returns nil if the object is not found.
|
||||
func (prog *Program) packageLevelValue(obj types.Object) Value {
|
||||
if pkg, ok := prog.packages[obj.Pkg()]; ok {
|
||||
return pkg.values[obj]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FuncValue returns the concrete Function denoted by the source-level
|
||||
// named function obj, or nil if obj denotes an interface method.
|
||||
//
|
||||
// TODO(adonovan): check the invariant that obj.Type() matches the
|
||||
// result's Signature, both in the params/results and in the receiver.
|
||||
func (prog *Program) FuncValue(obj *types.Func) *Function {
|
||||
obj = obj.Origin()
|
||||
fn, _ := prog.packageLevelValue(obj).(*Function)
|
||||
return fn
|
||||
}
|
||||
|
||||
// ConstValue returns the IR Value denoted by the source-level named
|
||||
// constant obj.
|
||||
func (prog *Program) ConstValue(obj *types.Const) *Const {
|
||||
// TODO(adonovan): opt: share (don't reallocate)
|
||||
// Consts for const objects and constant ast.Exprs.
|
||||
|
||||
// Universal constant? {true,false,nil}
|
||||
if obj.Parent() == types.Universe {
|
||||
return NewConst(obj.Val(), obj.Type())
|
||||
}
|
||||
// Package-level named constant?
|
||||
if v := prog.packageLevelValue(obj); v != nil {
|
||||
return v.(*Const)
|
||||
}
|
||||
return NewConst(obj.Val(), obj.Type())
|
||||
}
|
||||
|
||||
// VarValue returns the IR Value that corresponds to a specific
|
||||
// identifier denoting the source-level named variable obj.
|
||||
//
|
||||
// VarValue returns nil if a local variable was not found, perhaps
|
||||
// because its package was not built, the debug information was not
|
||||
// requested during IR construction, or the value was optimized away.
|
||||
//
|
||||
// ref is the path to an ast.Ident (e.g. from PathEnclosingInterval),
|
||||
// and that ident must resolve to obj.
|
||||
//
|
||||
// pkg is the package enclosing the reference. (A reference to a var
|
||||
// always occurs within a function, so we need to know where to find it.)
|
||||
//
|
||||
// If the identifier is a field selector and its base expression is
|
||||
// non-addressable, then VarValue returns the value of that field.
|
||||
// For example:
|
||||
//
|
||||
// func f() struct {x int}
|
||||
// f().x // VarValue(x) returns a *Field instruction of type int
|
||||
//
|
||||
// All other identifiers denote addressable locations (variables).
|
||||
// For them, VarValue may return either the variable's address or its
|
||||
// value, even when the expression is evaluated only for its value; the
|
||||
// situation is reported by isAddr, the second component of the result.
|
||||
//
|
||||
// If !isAddr, the returned value is the one associated with the
|
||||
// specific identifier. For example,
|
||||
//
|
||||
// var x int // VarValue(x) returns Const 0 here
|
||||
// x = 1 // VarValue(x) returns Const 1 here
|
||||
//
|
||||
// It is not specified whether the value or the address is returned in
|
||||
// any particular case, as it may depend upon optimizations performed
|
||||
// during IR code generation, such as registerization, constant
|
||||
// folding, avoidance of materialization of subexpressions, etc.
|
||||
func (prog *Program) VarValue(obj *types.Var, pkg *Package, ref []ast.Node) (value Value, isAddr bool) {
|
||||
// All references to a var are local to some function, possibly init.
|
||||
fn := EnclosingFunction(pkg, ref)
|
||||
if fn == nil {
|
||||
return // e.g. def of struct field; IR not built?
|
||||
}
|
||||
|
||||
id := ref[0].(*ast.Ident)
|
||||
|
||||
// Defining ident of a parameter?
|
||||
if id.Pos() == obj.Pos() {
|
||||
for _, param := range fn.Params {
|
||||
if param.Object() == obj {
|
||||
return param, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other ident?
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
if dr, ok := instr.(*DebugRef); ok {
|
||||
if dr.Pos() == id.Pos() {
|
||||
return dr.X, dr.IsAddr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defining ident of package-level var?
|
||||
if v := prog.packageLevelValue(obj); v != nil {
|
||||
return v.(*Global), true
|
||||
}
|
||||
|
||||
return // e.g. debug info not requested, or var optimized away
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//lint:file-ignore SA1019 go/ssa's test suite is built around the deprecated go/loader. We'll leave fixing that to upstream.
|
||||
|
||||
package ir_test
|
||||
|
||||
// This file defines tests of source-level debugging utilities.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"honnef.co/go/tools/go/ast/astutil"
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
"golang.org/x/tools/go/expect"
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
func TestObjValueLookup(t *testing.T) {
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skipf("no testdata directory on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
conf := loader.Config{ParserMode: parser.ParseComments}
|
||||
src, err := ioutil.ReadFile(filepath.Join(analysistest.TestData(), "objlookup.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
readFile := func(filename string) ([]byte, error) { return src, nil }
|
||||
f, err := conf.ParseFile(filepath.Join(analysistest.TestData(), "objlookup.go"), src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf.CreateFromFiles("main", f)
|
||||
|
||||
// Maps each var Ident (represented "name:linenum") to the
|
||||
// kind of ir.Value we expect (represented "Constant", "&Alloc").
|
||||
expectations := make(map[string]string)
|
||||
|
||||
// Each note of the form @ir(x, "BinOp") in testdata/objlookup.go
|
||||
// specifies an expectation that an object named x declared on the
|
||||
// same line is associated with an an ir.Value of type *ir.BinOp.
|
||||
notes, err := expect.ExtractGo(conf.Fset, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range notes {
|
||||
if n.Name != "ir" {
|
||||
t.Errorf("%v: unexpected note type %q, want \"ir\"", conf.Fset.Position(n.Pos), n.Name)
|
||||
continue
|
||||
}
|
||||
if len(n.Args) != 2 {
|
||||
t.Errorf("%v: ir has %d args, want 2", conf.Fset.Position(n.Pos), len(n.Args))
|
||||
continue
|
||||
}
|
||||
ident, ok := n.Args[0].(expect.Identifier)
|
||||
if !ok {
|
||||
t.Errorf("%v: got %v for arg 1, want identifier", conf.Fset.Position(n.Pos), n.Args[0])
|
||||
continue
|
||||
}
|
||||
exp, ok := n.Args[1].(string)
|
||||
if !ok {
|
||||
t.Errorf("%v: got %v for arg 2, want string", conf.Fset.Position(n.Pos), n.Args[1])
|
||||
continue
|
||||
}
|
||||
p, _, err := expect.MatchBefore(conf.Fset, readFile, n.Pos, string(ident))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
pos := conf.Fset.Position(p)
|
||||
key := fmt.Sprintf("%s:%d", ident, pos.Line)
|
||||
expectations[key] = exp
|
||||
}
|
||||
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
prog := irutil.CreateProgram(iprog, 0 /*|ir.PrintFunctions*/)
|
||||
mainInfo := iprog.Created[0]
|
||||
mainPkg := prog.Package(mainInfo.Pkg)
|
||||
mainPkg.SetDebugMode(true)
|
||||
mainPkg.Build()
|
||||
|
||||
var varIds []*ast.Ident
|
||||
var varObjs []*types.Var
|
||||
for id, obj := range mainInfo.Defs {
|
||||
// Check invariants for func and const objects.
|
||||
switch obj := obj.(type) {
|
||||
case *types.Func:
|
||||
checkFuncValue(t, prog, obj)
|
||||
|
||||
case *types.Const:
|
||||
checkConstValue(t, prog, obj)
|
||||
|
||||
case *types.Var:
|
||||
if id.Name == "_" {
|
||||
continue
|
||||
}
|
||||
varIds = append(varIds, id)
|
||||
varObjs = append(varObjs, obj)
|
||||
}
|
||||
}
|
||||
for id, obj := range mainInfo.Uses {
|
||||
if obj, ok := obj.(*types.Var); ok {
|
||||
varIds = append(varIds, id)
|
||||
varObjs = append(varObjs, obj)
|
||||
}
|
||||
}
|
||||
|
||||
// Check invariants for var objects.
|
||||
// The result varies based on the specific Ident.
|
||||
for i, id := range varIds {
|
||||
obj := varObjs[i]
|
||||
ref, _ := astutil.PathEnclosingInterval(f, id.Pos(), id.Pos())
|
||||
pos := prog.Fset.Position(id.Pos())
|
||||
exp := expectations[fmt.Sprintf("%s:%d", id.Name, pos.Line)]
|
||||
if exp == "" {
|
||||
t.Errorf("%s: no expectation for var ident %s ", pos, id.Name)
|
||||
continue
|
||||
}
|
||||
wantAddr := false
|
||||
if exp[0] == '&' {
|
||||
wantAddr = true
|
||||
exp = exp[1:]
|
||||
}
|
||||
checkVarValue(t, prog, mainPkg, ref, obj, exp, wantAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func checkFuncValue(t *testing.T, prog *ir.Program, obj *types.Func) {
|
||||
fn := prog.FuncValue(obj)
|
||||
// fmt.Printf("FuncValue(%s) = %s\n", obj, fn) // debugging
|
||||
if fn == nil {
|
||||
if obj.Name() != "interfaceMethod" {
|
||||
t.Errorf("FuncValue(%s) == nil", obj)
|
||||
}
|
||||
return
|
||||
}
|
||||
if fnobj := fn.Object(); fnobj != obj {
|
||||
t.Errorf("FuncValue(%s).Object() == %s; value was %s",
|
||||
obj, fnobj, fn.Name())
|
||||
return
|
||||
}
|
||||
if !types.Identical(fn.Type(), obj.Type()) {
|
||||
t.Errorf("FuncValue(%s).Type() == %s", obj, fn.Type())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func checkConstValue(t *testing.T, prog *ir.Program, obj *types.Const) {
|
||||
c := prog.ConstValue(obj)
|
||||
// fmt.Printf("ConstValue(%s) = %s\n", obj, c) // debugging
|
||||
if c == nil {
|
||||
t.Errorf("ConstValue(%s) == nil", obj)
|
||||
return
|
||||
}
|
||||
if !types.Identical(c.Type(), obj.Type()) {
|
||||
t.Errorf("ConstValue(%s).Type() == %s", obj, c.Type())
|
||||
return
|
||||
}
|
||||
if obj.Name() != "nil" {
|
||||
if !constant.Compare(c.Value, token.EQL, obj.Val()) {
|
||||
t.Errorf("ConstValue(%s).Value (%s) != %s",
|
||||
obj, c.Value, obj.Val())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkVarValue(t *testing.T, prog *ir.Program, pkg *ir.Package, ref []ast.Node, obj *types.Var, expKind string, wantAddr bool) {
|
||||
// The prefix of all assertions messages.
|
||||
prefix := fmt.Sprintf("VarValue(%s @ L%d)",
|
||||
obj, prog.Fset.Position(ref[0].Pos()).Line)
|
||||
|
||||
v, gotAddr := prog.VarValue(obj, pkg, ref)
|
||||
|
||||
// Kind is the concrete type of the ir Value.
|
||||
gotKind := "nil"
|
||||
if v != nil {
|
||||
gotKind = fmt.Sprintf("%T", v)[len("*ir."):]
|
||||
}
|
||||
|
||||
// fmt.Printf("%s = %v (kind %q; expect %q) wantAddr=%t gotAddr=%t\n", prefix, v, gotKind, expKind, wantAddr, gotAddr) // debugging
|
||||
|
||||
// Check the kinds match.
|
||||
// "nil" indicates expected failure (e.g. optimized away).
|
||||
if expKind != gotKind {
|
||||
t.Errorf("%s concrete type == %s, want %s", prefix, gotKind, expKind)
|
||||
}
|
||||
|
||||
// Check the types match.
|
||||
// If wantAddr, the expected type is the object's address.
|
||||
if v != nil {
|
||||
expType := obj.Type()
|
||||
if wantAddr {
|
||||
expType = types.NewPointer(expType)
|
||||
if !gotAddr {
|
||||
t.Errorf("%s: got value, want address", prefix)
|
||||
}
|
||||
} else if gotAddr {
|
||||
t.Errorf("%s: got address, want value", prefix)
|
||||
}
|
||||
if !types.Identical(v.Type(), expType) {
|
||||
t.Errorf("%s.Type() == %s, want %s", prefix, v.Type(), expType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that, in debug mode, we can determine the ir.Value
|
||||
// corresponding to every ast.Expr.
|
||||
func TestValueForExpr(t *testing.T) {
|
||||
testValueForExpr(t, filepath.Join(analysistest.TestData(), "valueforexpr.go"))
|
||||
}
|
||||
|
||||
func testValueForExpr(t *testing.T, testfile string) {
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skipf("no testdata dir on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
conf := loader.Config{ParserMode: parser.ParseComments}
|
||||
f, err := conf.ParseFile(testfile, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
conf.CreateFromFiles("main", f)
|
||||
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
mainInfo := iprog.Created[0]
|
||||
|
||||
prog := irutil.CreateProgram(iprog, 0)
|
||||
mainPkg := prog.Package(mainInfo.Pkg)
|
||||
mainPkg.SetDebugMode(true)
|
||||
mainPkg.Build()
|
||||
|
||||
if false {
|
||||
// debugging
|
||||
for _, mem := range mainPkg.Members {
|
||||
if fn, ok := mem.(*ir.Function); ok {
|
||||
fn.WriteTo(os.Stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parenExprs []*ast.ParenExpr
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
if n != nil {
|
||||
if e, ok := n.(*ast.ParenExpr); ok {
|
||||
parenExprs = append(parenExprs, e)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
notes, err := expect.ExtractGo(prog.Fset, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range notes {
|
||||
want := n.Name
|
||||
if want == "nil" {
|
||||
want = "<nil>"
|
||||
}
|
||||
position := prog.Fset.Position(n.Pos)
|
||||
var e ast.Expr
|
||||
for _, paren := range parenExprs {
|
||||
if paren.Pos() > n.Pos {
|
||||
e = paren.X
|
||||
break
|
||||
}
|
||||
}
|
||||
if e == nil {
|
||||
t.Errorf("%s: note doesn't precede ParenExpr: %q", position, want)
|
||||
continue
|
||||
}
|
||||
|
||||
path, _ := astutil.PathEnclosingInterval(f, n.Pos, n.Pos)
|
||||
if path == nil {
|
||||
t.Errorf("%s: can't find AST path from root to comment: %s", position, want)
|
||||
continue
|
||||
}
|
||||
|
||||
fn := ir.EnclosingFunction(mainPkg, path)
|
||||
if fn == nil {
|
||||
t.Errorf("%s: can't find enclosing function", position)
|
||||
continue
|
||||
}
|
||||
|
||||
v, gotAddr := fn.ValueForExpr(e) // (may be nil)
|
||||
got := strings.TrimPrefix(fmt.Sprintf("%T", v), "*ir.")
|
||||
if got != want {
|
||||
t.Errorf("%s: got value %q, want %q", position, got, want)
|
||||
}
|
||||
if v != nil {
|
||||
T := v.Type()
|
||||
if gotAddr {
|
||||
T = T.Underlying().(*types.Pointer).Elem() // deref
|
||||
}
|
||||
if !types.Identical(T, mainInfo.TypeOf(e)) {
|
||||
t.Errorf("%s: got type %s, want %s", position, mainInfo.TypeOf(e), T)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findInterval parses input and returns the [start, end) positions of
|
||||
// the first occurrence of substr in input. f==nil indicates failure;
|
||||
// an error has already been reported in that case.
|
||||
func findInterval(t *testing.T, fset *token.FileSet, input, substr string) (f *ast.File, start, end token.Pos) {
|
||||
f, err := parser.ParseFile(fset, "<input>", input, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
t.Errorf("parse error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
i := strings.Index(input, substr)
|
||||
if i < 0 {
|
||||
t.Errorf("%q is not a substring of input", substr)
|
||||
f = nil
|
||||
return
|
||||
}
|
||||
|
||||
filePos := fset.File(f.Package)
|
||||
return f, filePos.Pos(i), filePos.Pos(i + len(substr))
|
||||
}
|
||||
|
||||
func TestEnclosingFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string // the input file
|
||||
substr string // first occurrence of this string denotes interval
|
||||
fn string // name of expected containing function
|
||||
}{
|
||||
// We use distinctive numbers as syntactic landmarks.
|
||||
|
||||
// Ordinary function:
|
||||
{`package main
|
||||
func f() { println(1003) }`,
|
||||
"100", "main.f"},
|
||||
// Methods:
|
||||
{`package main
|
||||
type T int
|
||||
func (t T) f() { println(200) }`,
|
||||
"200", "(main.T).f"},
|
||||
// Function literal:
|
||||
{`package main
|
||||
func f() { println(func() { print(300) }) }`,
|
||||
"300", "main.f$1"},
|
||||
// Doubly nested
|
||||
{`package main
|
||||
func f() { println(func() { print(func() { print(350) })})}`,
|
||||
"350", "main.f$1$1"},
|
||||
// Implicit init for package-level var initializer.
|
||||
{"package main; var a = 400", "400", "main.init"},
|
||||
// No code for constants:
|
||||
{"package main; const a = 500", "500", "(none)"},
|
||||
// Explicit init()
|
||||
{"package main; func init() { println(600) }", "600", "main.init#1"},
|
||||
// Multiple explicit init functions:
|
||||
{`package main
|
||||
func init() { println("foo") }
|
||||
func init() { println(800) }`,
|
||||
"800", "main.init#2"},
|
||||
// init() containing FuncLit.
|
||||
{`package main
|
||||
func init() { println(func(){print(900)}) }`,
|
||||
"900", "main.init#1$1"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
conf := loader.Config{Fset: token.NewFileSet()}
|
||||
f, start, end := findInterval(t, conf.Fset, test.input, test.substr)
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
path, exact := astutil.PathEnclosingInterval(f, start, end)
|
||||
if !exact {
|
||||
t.Errorf("EnclosingFunction(%q) not exact", test.substr)
|
||||
continue
|
||||
}
|
||||
|
||||
conf.CreateFromFiles("main", f)
|
||||
|
||||
iprog, err := conf.Load()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
prog := irutil.CreateProgram(iprog, 0)
|
||||
pkg := prog.Package(iprog.Created[0].Pkg)
|
||||
pkg.Build()
|
||||
|
||||
name := "(none)"
|
||||
fn := ir.EnclosingFunction(pkg, path)
|
||||
if fn != nil {
|
||||
name = fn.String()
|
||||
}
|
||||
|
||||
if name != test.fn {
|
||||
t.Errorf("EnclosingFunction(%q in %q) got %s, want %s",
|
||||
test.substr, test.input, name, test.fn)
|
||||
continue
|
||||
}
|
||||
|
||||
// While we're here: test HasEnclosingFunction.
|
||||
if has := ir.HasEnclosingFunction(pkg, path); has != (fn != nil) {
|
||||
t.Errorf("HasEnclosingFunction(%q in %q) got %v, want %v",
|
||||
test.substr, test.input, has, fn != nil)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user