whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
// Copyright 2024 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 (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/internal/analysisinternal"
|
||||
)
|
||||
|
||||
func TestAddImport(t *testing.T) {
|
||||
descr := func(s string) string {
|
||||
if _, _, line, ok := runtime.Caller(1); ok {
|
||||
return fmt.Sprintf("L%d %s", line, s)
|
||||
}
|
||||
panic("runtime.Caller failed")
|
||||
}
|
||||
|
||||
// Each test case contains a «name pkgpath»
|
||||
// section to be replaced with a reference
|
||||
// to a valid import of pkgpath,
|
||||
// ideally of the specified name.
|
||||
for _, test := range []struct {
|
||||
descr, src, want string
|
||||
}{
|
||||
{
|
||||
descr: descr("simple add import"),
|
||||
src: `package a
|
||||
func _() {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
import "fmt"
|
||||
|
||||
func _() {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing import"),
|
||||
src: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt.Stringer) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt.Stringer) {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing blank import"),
|
||||
src: `package a
|
||||
|
||||
import _ "fmt"
|
||||
|
||||
func _() {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
import _ "fmt"
|
||||
|
||||
func _() {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing renaming import"),
|
||||
src: `package a
|
||||
|
||||
import fmtpkg "fmt"
|
||||
|
||||
var fmt int
|
||||
|
||||
func _(fmtpkg.Stringer) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import fmtpkg "fmt"
|
||||
|
||||
var fmt int
|
||||
|
||||
func _(fmtpkg.Stringer) {
|
||||
fmtpkg
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing import is shadowed"),
|
||||
src: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
var _ fmt.Stringer
|
||||
|
||||
func _(fmt int) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import fmt0 "fmt"
|
||||
|
||||
import "fmt"
|
||||
|
||||
var _ fmt.Stringer
|
||||
|
||||
func _(fmt int) {
|
||||
fmt0
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("preferred name is shadowed"),
|
||||
src: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt fmt.Stringer) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import fmt0 "fmt"
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt fmt.Stringer) {
|
||||
fmt0
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("import inserted before doc comments"),
|
||||
src: `package a
|
||||
|
||||
// hello
|
||||
import ()
|
||||
|
||||
// world
|
||||
func _() {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
// hello
|
||||
import ()
|
||||
|
||||
// world
|
||||
func _() {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("arbitrary preferred name => renaming import"),
|
||||
src: `package a
|
||||
|
||||
func _() {
|
||||
«foo encoding/json»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import foo "encoding/json"
|
||||
|
||||
func _() {
|
||||
foo
|
||||
}`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.descr, func(t *testing.T) {
|
||||
// splice marker
|
||||
before, mid, ok1 := strings.Cut(test.src, "«")
|
||||
mid, after, ok2 := strings.Cut(mid, "»")
|
||||
if !ok1 || !ok2 {
|
||||
t.Fatal("no «name path» marker")
|
||||
}
|
||||
src := before + "/*!*/" + after
|
||||
name, path, _ := strings.Cut(mid, " ")
|
||||
|
||||
// parse
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "a.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
pos := fset.File(f.Pos()).Pos(len(before))
|
||||
|
||||
// type-check
|
||||
info := &types.Info{
|
||||
Types: make(map[ast.Expr]types.TypeAndValue),
|
||||
Scopes: make(map[ast.Node]*types.Scope),
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
Implicits: make(map[ast.Node]types.Object),
|
||||
}
|
||||
conf := &types.Config{
|
||||
Error: func(err error) { t.Log(err) },
|
||||
Importer: importer.Default(),
|
||||
}
|
||||
conf.Check(f.Name.Name, fset, []*ast.File{f}, info)
|
||||
|
||||
// add import
|
||||
name, edits := analysisinternal.AddImport(info, f, pos, path, name)
|
||||
|
||||
var edit analysis.TextEdit
|
||||
switch len(edits) {
|
||||
case 0:
|
||||
case 1:
|
||||
edit = edits[0]
|
||||
default:
|
||||
t.Fatalf("expected at most one edit, got %d", len(edits))
|
||||
}
|
||||
|
||||
// apply patch
|
||||
start := fset.Position(edit.Pos)
|
||||
end := fset.Position(edit.End)
|
||||
output := src[:start.Offset] + string(edit.NewText) + src[end.Offset:]
|
||||
output = strings.ReplaceAll(output, "/*!*/", name)
|
||||
if output != test.want {
|
||||
t.Errorf("\n--got--\n%s\n--want--\n%s\n--diff--\n%s",
|
||||
output, test.want, cmp.Diff(test.want, output))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// 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"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
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 {
|
||||
// TODO(adonovan): think about generics, and also generic aliases.
|
||||
under := aliases.Unalias(typ)
|
||||
// Don't call Underlying unconditionally: although it removes
|
||||
// Named and Alias, it also removes TypeParam.
|
||||
if n, ok := under.(*types.Named); ok {
|
||||
under = n.Underlying()
|
||||
}
|
||||
switch under := under.(type) {
|
||||
case *types.Basic:
|
||||
switch {
|
||||
case under.Info()&types.IsNumeric != 0:
|
||||
return &ast.BasicLit{Kind: token.INT, Value: "0"}
|
||||
case under.Info()&types.IsBoolean != 0:
|
||||
return &ast.Ident{Name: "false"}
|
||||
case under.Info()&types.IsString != 0:
|
||||
return &ast.BasicLit{Kind: token.STRING, Value: `""`}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown basic type %v", under))
|
||||
}
|
||||
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(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if t.Variadic() {
|
||||
last := params[len(params)-1]
|
||||
last.Type = &ast.Ellipsis{Elt: last.Type.(*ast.ArrayType).Elt}
|
||||
}
|
||||
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 interface{ Obj() *types.TypeName }: // *types.{Alias,Named,TypeParam}
|
||||
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
|
||||
}
|
||||
case *ast.SwitchStmt, *ast.TypeSwitchStmt:
|
||||
return expr.(ast.Stmt)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// MakeReadFile returns a simple implementation of the Pass.ReadFile function.
|
||||
func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) {
|
||||
return func(filename string) ([]byte, error) {
|
||||
if err := CheckReadable(pass, filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(filename)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
|
||||
func CheckReadable(pass *analysis.Pass, filename string) error {
|
||||
if slicesContains(pass.OtherFiles, filename) ||
|
||||
slicesContains(pass.IgnoredFiles, filename) {
|
||||
return nil
|
||||
}
|
||||
for _, f := range pass.Files {
|
||||
// TODO(adonovan): use go1.20 f.FileStart
|
||||
if pass.Fset.File(f.Pos()).Name() == filename {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
|
||||
}
|
||||
|
||||
// TODO(adonovan): use go1.21 slices.Contains.
|
||||
func slicesContains[S ~[]E, E comparable](slice S, x E) bool {
|
||||
for _, elem := range slice {
|
||||
if elem == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddImport checks whether this file already imports pkgpath and
|
||||
// that import is in scope at pos. If so, it returns the name under
|
||||
// which it was imported and a zero edit. Otherwise, it adds a new
|
||||
// import of pkgpath, using a name derived from the preferred name,
|
||||
// and returns the chosen name along with the edit for the new import.
|
||||
//
|
||||
// It does not mutate its arguments.
|
||||
func AddImport(info *types.Info, file *ast.File, pos token.Pos, pkgpath, preferredName string) (name string, newImport []analysis.TextEdit) {
|
||||
// Find innermost enclosing lexical block.
|
||||
scope := info.Scopes[file].Innermost(pos)
|
||||
if scope == nil {
|
||||
panic("no enclosing lexical block")
|
||||
}
|
||||
|
||||
// Is there an existing import of this package?
|
||||
// If so, are we in its scope? (not shadowed)
|
||||
for _, spec := range file.Imports {
|
||||
pkgname, ok := importedPkgName(info, spec)
|
||||
if ok && pkgname.Imported().Path() == pkgpath {
|
||||
if _, obj := scope.LookupParent(pkgname.Name(), pos); obj == pkgname {
|
||||
return pkgname.Name(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must add a new import.
|
||||
// Ensure we have a fresh name.
|
||||
newName := preferredName
|
||||
for i := 0; ; i++ {
|
||||
if _, obj := scope.LookupParent(newName, pos); obj == nil {
|
||||
break // fresh
|
||||
}
|
||||
newName = fmt.Sprintf("%s%d", preferredName, i)
|
||||
}
|
||||
|
||||
// For now, keep it real simple: create a new import
|
||||
// declaration before the first existing declaration (which
|
||||
// must exist), including its comments, and let goimports tidy it up.
|
||||
//
|
||||
// Use a renaming import whenever the preferred name is not
|
||||
// available, or the chosen name does not match the last
|
||||
// segment of its path.
|
||||
newText := fmt.Sprintf("import %q\n\n", pkgpath)
|
||||
if newName != preferredName || newName != pathpkg.Base(pkgpath) {
|
||||
newText = fmt.Sprintf("import %s %q\n\n", newName, pkgpath)
|
||||
}
|
||||
decl0 := file.Decls[0]
|
||||
var before ast.Node = decl0
|
||||
switch decl0 := decl0.(type) {
|
||||
case *ast.GenDecl:
|
||||
if decl0.Doc != nil {
|
||||
before = decl0.Doc
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
if decl0.Doc != nil {
|
||||
before = decl0.Doc
|
||||
}
|
||||
}
|
||||
return newName, []analysis.TextEdit{{
|
||||
Pos: before.Pos(),
|
||||
End: before.Pos(),
|
||||
NewText: []byte(newText),
|
||||
}}
|
||||
}
|
||||
|
||||
// importedPkgName returns the PkgName object declared by an ImportSpec.
|
||||
// TODO(adonovan): use go1.22's Info.PkgNameOf.
|
||||
func importedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) {
|
||||
var obj types.Object
|
||||
if imp.Name != nil {
|
||||
obj = info.Defs[imp.Name]
|
||||
} else {
|
||||
obj = info.Implicits[imp]
|
||||
}
|
||||
pkgname, ok := obj.(*types.PkgName)
|
||||
return pkgname, ok
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user