whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,386 @@
// Copyright 2020 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 analysisinternal provides gopls' internal analyses with a
// number of helper functions that operate on typed syntax trees.
package analysisinternal
import (
"bytes"
"fmt"
"go/ast"
"go/token"
"go/types"
"strconv"
)
func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
// Get the end position for the type error.
offset, end := fset.PositionFor(start, false).Offset, start
if offset >= len(src) {
return end
}
if width := bytes.IndexAny(src[offset:], " \n,():;[]+-*"); width > 0 {
end = start + token.Pos(width)
}
return end
}
func ZeroValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
under := typ
if n, ok := typ.(*types.Named); ok {
under = n.Underlying()
}
switch u := under.(type) {
case *types.Basic:
switch {
case u.Info()&types.IsNumeric != 0:
return &ast.BasicLit{Kind: token.INT, Value: "0"}
case u.Info()&types.IsBoolean != 0:
return &ast.Ident{Name: "false"}
case u.Info()&types.IsString != 0:
return &ast.BasicLit{Kind: token.STRING, Value: `""`}
default:
panic(fmt.Sprintf("unknown basic type %v", u))
}
case *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Signature, *types.Slice, *types.Array:
return ast.NewIdent("nil")
case *types.Struct:
texpr := TypeExpr(f, pkg, typ) // typ because we want the name here.
if texpr == nil {
return nil
}
return &ast.CompositeLit{
Type: texpr,
}
}
return nil
}
// IsZeroValue checks whether the given expression is a 'zero value' (as determined by output of
// analysisinternal.ZeroValue)
func IsZeroValue(expr ast.Expr) bool {
switch e := expr.(type) {
case *ast.BasicLit:
return e.Value == "0" || e.Value == `""`
case *ast.Ident:
return e.Name == "nil" || e.Name == "false"
default:
return false
}
}
// TypeExpr returns syntax for the specified type. References to
// named types from packages other than pkg are qualified by an appropriate
// package name, as defined by the import environment of file.
func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
switch t := typ.(type) {
case *types.Basic:
switch t.Kind() {
case types.UnsafePointer:
return &ast.SelectorExpr{X: ast.NewIdent("unsafe"), Sel: ast.NewIdent("Pointer")}
default:
return ast.NewIdent(t.Name())
}
case *types.Pointer:
x := TypeExpr(f, pkg, t.Elem())
if x == nil {
return nil
}
return &ast.UnaryExpr{
Op: token.MUL,
X: x,
}
case *types.Array:
elt := TypeExpr(f, pkg, t.Elem())
if elt == nil {
return nil
}
return &ast.ArrayType{
Len: &ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprintf("%d", t.Len()),
},
Elt: elt,
}
case *types.Slice:
elt := TypeExpr(f, pkg, t.Elem())
if elt == nil {
return nil
}
return &ast.ArrayType{
Elt: elt,
}
case *types.Map:
key := TypeExpr(f, pkg, t.Key())
value := TypeExpr(f, pkg, t.Elem())
if key == nil || value == nil {
return nil
}
return &ast.MapType{
Key: key,
Value: value,
}
case *types.Chan:
dir := ast.ChanDir(t.Dir())
if t.Dir() == types.SendRecv {
dir = ast.SEND | ast.RECV
}
value := TypeExpr(f, pkg, t.Elem())
if value == nil {
return nil
}
return &ast.ChanType{
Dir: dir,
Value: value,
}
case *types.Signature:
var params []*ast.Field
for i := 0; i < t.Params().Len(); i++ {
p := TypeExpr(f, pkg, t.Params().At(i).Type())
if p == nil {
return nil
}
params = append(params, &ast.Field{
Type: p,
Names: []*ast.Ident{
{
Name: t.Params().At(i).Name(),
},
},
})
}
var returns []*ast.Field
for i := 0; i < t.Results().Len(); i++ {
r := TypeExpr(f, pkg, t.Results().At(i).Type())
if r == nil {
return nil
}
returns = append(returns, &ast.Field{
Type: r,
})
}
return &ast.FuncType{
Params: &ast.FieldList{
List: params,
},
Results: &ast.FieldList{
List: returns,
},
}
case *types.Named:
if t.Obj().Pkg() == nil {
return ast.NewIdent(t.Obj().Name())
}
if t.Obj().Pkg() == pkg {
return ast.NewIdent(t.Obj().Name())
}
pkgName := t.Obj().Pkg().Name()
// If the file already imports the package under another name, use that.
for _, cand := range f.Imports {
if path, _ := strconv.Unquote(cand.Path.Value); path == t.Obj().Pkg().Path() {
if cand.Name != nil && cand.Name.Name != "" {
pkgName = cand.Name.Name
}
}
}
if pkgName == "." {
return ast.NewIdent(t.Obj().Name())
}
return &ast.SelectorExpr{
X: ast.NewIdent(pkgName),
Sel: ast.NewIdent(t.Obj().Name()),
}
case *types.Struct:
return ast.NewIdent(t.String())
case *types.Interface:
return ast.NewIdent(t.String())
default:
return nil
}
}
// StmtToInsertVarBefore returns the ast.Stmt before which we can safely insert a new variable.
// Some examples:
//
// Basic Example:
// z := 1
// y := z + x
// If x is undeclared, then this function would return `y := z + x`, so that we
// can insert `x := ` on the line before `y := z + x`.
//
// If stmt example:
// if z == 1 {
// } else if z == y {}
// If y is undeclared, then this function would return `if z == 1 {`, because we cannot
// insert a statement between an if and an else if statement. As a result, we need to find
// the top of the if chain to insert `y := ` before.
func StmtToInsertVarBefore(path []ast.Node) ast.Stmt {
enclosingIndex := -1
for i, p := range path {
if _, ok := p.(ast.Stmt); ok {
enclosingIndex = i
break
}
}
if enclosingIndex == -1 {
return nil
}
enclosingStmt := path[enclosingIndex]
switch enclosingStmt.(type) {
case *ast.IfStmt:
// The enclosingStmt is inside of the if declaration,
// We need to check if we are in an else-if stmt and
// get the base if statement.
return baseIfStmt(path, enclosingIndex)
case *ast.CaseClause:
// Get the enclosing switch stmt if the enclosingStmt is
// inside of the case statement.
for i := enclosingIndex + 1; i < len(path); i++ {
if node, ok := path[i].(*ast.SwitchStmt); ok {
return node
} else if node, ok := path[i].(*ast.TypeSwitchStmt); ok {
return node
}
}
}
if len(path) <= enclosingIndex+1 {
return enclosingStmt.(ast.Stmt)
}
// Check if the enclosing statement is inside another node.
switch expr := path[enclosingIndex+1].(type) {
case *ast.IfStmt:
// Get the base if statement.
return baseIfStmt(path, enclosingIndex+1)
case *ast.ForStmt:
if expr.Init == enclosingStmt || expr.Post == enclosingStmt {
return expr
}
}
return enclosingStmt.(ast.Stmt)
}
// baseIfStmt walks up the if/else-if chain until we get to
// the top of the current if chain.
func baseIfStmt(path []ast.Node, index int) ast.Stmt {
stmt := path[index]
for i := index + 1; i < len(path); i++ {
if node, ok := path[i].(*ast.IfStmt); ok && node.Else == stmt {
stmt = node
continue
}
break
}
return stmt.(ast.Stmt)
}
// WalkASTWithParent walks the AST rooted at n. The semantics are
// similar to ast.Inspect except it does not call f(nil).
func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
var ancestors []ast.Node
ast.Inspect(n, func(n ast.Node) (recurse bool) {
if n == nil {
ancestors = ancestors[:len(ancestors)-1]
return false
}
var parent ast.Node
if len(ancestors) > 0 {
parent = ancestors[len(ancestors)-1]
}
ancestors = append(ancestors, n)
return f(n, parent)
})
}
// MatchingIdents finds the names of all identifiers in 'node' that match any of the given types.
// 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within
// the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that
// is unrecognized.
func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string {
// Initialize matches to contain the variable types we are searching for.
matches := make(map[types.Type][]string)
for _, typ := range typs {
if typ == nil {
continue // TODO(adonovan): is this reachable?
}
matches[typ] = nil // create entry
}
seen := map[types.Object]struct{}{}
ast.Inspect(node, func(n ast.Node) bool {
if n == nil {
return false
}
// Prevent circular definitions. If 'pos' is within an assignment statement, do not
// allow any identifiers in that assignment statement to be selected. Otherwise,
// we could do the following, where 'x' satisfies the type of 'f0':
//
// x := fakeStruct{f0: x}
//
if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() {
return false
}
if n.End() > pos {
return n.Pos() <= pos
}
ident, ok := n.(*ast.Ident)
if !ok || ident.Name == "_" {
return true
}
obj := info.Defs[ident]
if obj == nil || obj.Type() == nil {
return true
}
if _, ok := obj.(*types.TypeName); ok {
return true
}
// Prevent duplicates in matches' values.
if _, ok = seen[obj]; ok {
return true
}
seen[obj] = struct{}{}
// Find the scope for the given position. Then, check whether the object
// exists within the scope.
innerScope := pkg.Scope().Innermost(pos)
if innerScope == nil {
return true
}
_, foundObj := innerScope.LookupParent(ident.Name, pos)
if foundObj != obj {
return true
}
// The object must match one of the types that we are searching for.
// TODO(adonovan): opt: use typeutil.Map?
if names, ok := matches[obj.Type()]; ok {
matches[obj.Type()] = append(names, ident.Name)
} else {
// If the object type does not exactly match
// any of the target types, greedily find the first
// target type that the object type can satisfy.
for typ := range matches {
if equivalentTypes(obj.Type(), typ) {
matches[typ] = append(matches[typ], ident.Name)
}
}
}
return true
})
return matches
}
func equivalentTypes(want, got types.Type) bool {
if types.Identical(want, got) {
return true
}
// Code segment to help check for untyped equality from (golang/go#32146).
if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 {
if lhs, ok := got.Underlying().(*types.Basic); ok {
return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType
}
}
return types.AssignableTo(want, got)
}
@@ -0,0 +1,113 @@
// Copyright 2023 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 analysisinternal
import (
"fmt"
"go/parser"
"go/token"
"strings"
)
// MustExtractDoc is like [ExtractDoc] but it panics on error.
//
// To use, define a doc.go file such as:
//
// // Package halting defines an analyzer of program termination.
// //
// // # Analyzer halting
// //
// // halting: reports whether execution will halt.
// //
// // The halting analyzer reports a diagnostic for functions
// // that run forever. To suppress the diagnostics, try inserting
// // a 'break' statement into each loop.
// package halting
//
// import _ "embed"
//
// //go:embed doc.go
// var doc string
//
// And declare your analyzer as:
//
// var Analyzer = &analysis.Analyzer{
// Name: "halting",
// Doc: analysisutil.MustExtractDoc(doc, "halting"),
// ...
// }
func MustExtractDoc(content, name string) string {
doc, err := ExtractDoc(content, name)
if err != nil {
panic(err)
}
return doc
}
// ExtractDoc extracts a section of a package doc comment from the
// provided contents of an analyzer package's doc.go file.
//
// A section is a portion of the comment between one heading and
// the next, using this form:
//
// # Analyzer NAME
//
// NAME: SUMMARY
//
// Full description...
//
// where NAME matches the name argument, and SUMMARY is a brief
// verb-phrase that describes the analyzer. The following lines, up
// until the next heading or the end of the comment, contain the full
// description. ExtractDoc returns the portion following the colon,
// which is the form expected by Analyzer.Doc.
//
// Example:
//
// # Analyzer printf
//
// printf: checks consistency of calls to printf
//
// The printf analyzer checks consistency of calls to printf.
// Here is the complete description...
//
// This notation allows a single doc comment to provide documentation
// for multiple analyzers, each in its own section.
// The HTML anchors generated for each heading are predictable.
//
// It returns an error if the content was not a valid Go source file
// containing a package doc comment with a heading of the required
// form.
//
// This machinery enables the package documentation (typically
// accessible via the web at https://pkg.go.dev/) and the command
// documentation (typically printed to a terminal) to be derived from
// the same source and formatted appropriately.
func ExtractDoc(content, name string) (string, error) {
if content == "" {
return "", fmt.Errorf("empty Go source file")
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", content, parser.ParseComments|parser.PackageClauseOnly)
if err != nil {
return "", fmt.Errorf("not a Go source file")
}
if f.Doc == nil {
return "", fmt.Errorf("Go source file has no package doc comment")
}
for _, section := range strings.Split(f.Doc.Text(), "\n# ") {
if body := strings.TrimPrefix(section, "Analyzer "+name); body != section &&
body != "" &&
body[0] == '\r' || body[0] == '\n' {
body = strings.TrimSpace(body)
rest := strings.TrimPrefix(body, name+":")
if rest == body {
return "", fmt.Errorf("'Analyzer %s' heading not followed by '%s: summary...' line", name, name)
}
return strings.TrimSpace(rest), nil
}
}
return "", fmt.Errorf("package doc comment contains no 'Analyzer %s' heading", name)
}
@@ -0,0 +1,80 @@
// Copyright 2023 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 analysisinternal_test
import (
"testing"
"golang.org/x/tools/internal/analysisinternal"
)
func TestExtractDoc(t *testing.T) {
const multi = `// Copyright
//+build tag
// Package foo
//
// # Irrelevant heading
//
// This is irrelevant doc.
//
// # Analyzer nocolon
//
// This one has the wrong form for this line.
//
// # Analyzer food
//
// food: reports dining opportunities
//
// This is the doc for analyzer 'food'.
//
// # Analyzer foo
//
// foo: reports diagnostics
//
// This is the doc for analyzer 'foo'.
//
// # Analyzer bar
//
// bar: reports drinking opportunities
//
// This is the doc for analyzer 'bar'.
package blah
var x = syntax error
`
for _, test := range []struct {
content, name string
want string // doc or "error: %w" string
}{
{"", "foo",
"error: empty Go source file"},
{"//foo", "foo",
"error: not a Go source file"},
{"//foo\npackage foo", "foo",
"error: package doc comment contains no 'Analyzer foo' heading"},
{multi, "foo",
"reports diagnostics\n\nThis is the doc for analyzer 'foo'."},
{multi, "bar",
"reports drinking opportunities\n\nThis is the doc for analyzer 'bar'."},
{multi, "food",
"reports dining opportunities\n\nThis is the doc for analyzer 'food'."},
{multi, "nope",
"error: package doc comment contains no 'Analyzer nope' heading"},
{multi, "nocolon",
"error: 'Analyzer nocolon' heading not followed by 'nocolon: summary...' line"},
} {
got, err := analysisinternal.ExtractDoc(test.content, test.name)
if err != nil {
got = "error: " + err.Error()
}
if test.want != got {
t.Errorf("ExtractDoc(%q) returned <<%s>>, want <<%s>>, given input <<%s>>",
test.name, got, test.want, test.content)
}
}
}