github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/refactor/rename/spec.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package rename
     6  
     7  // This file contains logic related to specifying a renaming: parsing of
     8  // the flags as a form of query, and finding the object(s) it denotes.
     9  // See Usage for flag details.
    10  
    11  import (
    12  	"bytes"
    13  	"fmt"
    14  	"go/ast"
    15  	"go/build"
    16  	"go/parser"
    17  	"go/token"
    18  	"go/types"
    19  	"log"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/powerman/golang-tools/go/buildutil"
    27  	"github.com/powerman/golang-tools/go/loader"
    28  )
    29  
    30  // A spec specifies an entity to rename.
    31  //
    32  // It is populated from an -offset flag or -from query;
    33  // see Usage for the allowed -from query forms.
    34  //
    35  type spec struct {
    36  	// pkg is the package containing the position
    37  	// specified by the -from or -offset flag.
    38  	// If filename == "", our search for the 'from' entity
    39  	// is restricted to this package.
    40  	pkg string
    41  
    42  	// The original name of the entity being renamed.
    43  	// If the query had a ::from component, this is that;
    44  	// otherwise it's the last segment, e.g.
    45  	//   (encoding/json.Decoder).from
    46  	//   encoding/json.from
    47  	fromName string
    48  
    49  	// -- The remaining fields are private to this file.  All are optional. --
    50  
    51  	// The query's ::x suffix, if any.
    52  	searchFor string
    53  
    54  	// e.g. "Decoder" in "(encoding/json.Decoder).fieldOrMethod"
    55  	//                or "encoding/json.Decoder
    56  	pkgMember string
    57  
    58  	// e.g. fieldOrMethod in "(encoding/json.Decoder).fieldOrMethod"
    59  	typeMember string
    60  
    61  	// Restricts the query to this file.
    62  	// Implied by -from="file.go::x" and -offset flags.
    63  	filename string
    64  
    65  	// Byte offset of the 'from' identifier within the file named 'filename'.
    66  	// -offset mode only.
    67  	offset int
    68  }
    69  
    70  // parseFromFlag interprets the "-from" flag value as a renaming specification.
    71  // See Usage in rename.go for valid formats.
    72  func parseFromFlag(ctxt *build.Context, fromFlag string) (*spec, error) {
    73  	var spec spec
    74  	var main string // sans "::x" suffix
    75  	switch parts := strings.Split(fromFlag, "::"); len(parts) {
    76  	case 1:
    77  		main = parts[0]
    78  	case 2:
    79  		main = parts[0]
    80  		spec.searchFor = parts[1]
    81  		if parts[1] == "" {
    82  			// error
    83  		}
    84  	default:
    85  		return nil, fmt.Errorf("-from %q: invalid identifier specification (see -help for formats)", fromFlag)
    86  	}
    87  
    88  	if strings.HasSuffix(main, ".go") {
    89  		// main is "filename.go"
    90  		if spec.searchFor == "" {
    91  			return nil, fmt.Errorf("-from: filename %q must have a ::name suffix", main)
    92  		}
    93  		spec.filename = main
    94  		if !buildutil.FileExists(ctxt, spec.filename) {
    95  			return nil, fmt.Errorf("no such file: %s", spec.filename)
    96  		}
    97  
    98  		bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  		spec.pkg = bp.ImportPath
   103  
   104  	} else {
   105  		// main is one of:
   106  		//  "importpath"
   107  		//  "importpath".member
   108  		//  (*"importpath".type).fieldormethod           (parens and star optional)
   109  		if err := parseObjectSpec(&spec, main); err != nil {
   110  			return nil, err
   111  		}
   112  	}
   113  
   114  	if spec.searchFor != "" {
   115  		spec.fromName = spec.searchFor
   116  	}
   117  
   118  	cwd, err := os.Getwd()
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	// Sanitize the package.
   124  	bp, err := ctxt.Import(spec.pkg, cwd, build.FindOnly)
   125  	if err != nil {
   126  		return nil, fmt.Errorf("can't find package %q", spec.pkg)
   127  	}
   128  	spec.pkg = bp.ImportPath
   129  
   130  	if !isValidIdentifier(spec.fromName) {
   131  		return nil, fmt.Errorf("-from: invalid identifier %q", spec.fromName)
   132  	}
   133  
   134  	if Verbose {
   135  		log.Printf("-from spec: %+v", spec)
   136  	}
   137  
   138  	return &spec, nil
   139  }
   140  
   141  // parseObjectSpec parses main as one of the non-filename forms of
   142  // object specification.
   143  func parseObjectSpec(spec *spec, main string) error {
   144  	// Parse main as a Go expression, albeit a strange one.
   145  	e, _ := parser.ParseExpr(main)
   146  
   147  	if pkg := parseImportPath(e); pkg != "" {
   148  		// e.g. bytes or "encoding/json": a package
   149  		spec.pkg = pkg
   150  		if spec.searchFor == "" {
   151  			return fmt.Errorf("-from %q: package import path %q must have a ::name suffix",
   152  				main, main)
   153  		}
   154  		return nil
   155  	}
   156  
   157  	if e, ok := e.(*ast.SelectorExpr); ok {
   158  		x := unparen(e.X)
   159  
   160  		// Strip off star constructor, if any.
   161  		if star, ok := x.(*ast.StarExpr); ok {
   162  			x = star.X
   163  		}
   164  
   165  		if pkg := parseImportPath(x); pkg != "" {
   166  			// package member e.g. "encoding/json".HTMLEscape
   167  			spec.pkg = pkg              // e.g. "encoding/json"
   168  			spec.pkgMember = e.Sel.Name // e.g. "HTMLEscape"
   169  			spec.fromName = e.Sel.Name
   170  			return nil
   171  		}
   172  
   173  		if x, ok := x.(*ast.SelectorExpr); ok {
   174  			// field/method of type e.g. ("encoding/json".Decoder).Decode
   175  			y := unparen(x.X)
   176  			if pkg := parseImportPath(y); pkg != "" {
   177  				spec.pkg = pkg               // e.g. "encoding/json"
   178  				spec.pkgMember = x.Sel.Name  // e.g. "Decoder"
   179  				spec.typeMember = e.Sel.Name // e.g. "Decode"
   180  				spec.fromName = e.Sel.Name
   181  				return nil
   182  			}
   183  		}
   184  	}
   185  
   186  	return fmt.Errorf("-from %q: invalid expression", main)
   187  }
   188  
   189  // parseImportPath returns the import path of the package denoted by e.
   190  // Any import path may be represented as a string literal;
   191  // single-segment import paths (e.g. "bytes") may also be represented as
   192  // ast.Ident.  parseImportPath returns "" for all other expressions.
   193  func parseImportPath(e ast.Expr) string {
   194  	switch e := e.(type) {
   195  	case *ast.Ident:
   196  		return e.Name // e.g. bytes
   197  
   198  	case *ast.BasicLit:
   199  		if e.Kind == token.STRING {
   200  			pkgname, _ := strconv.Unquote(e.Value)
   201  			return pkgname // e.g. "encoding/json"
   202  		}
   203  	}
   204  	return ""
   205  }
   206  
   207  // parseOffsetFlag interprets the "-offset" flag value as a renaming specification.
   208  func parseOffsetFlag(ctxt *build.Context, offsetFlag string) (*spec, error) {
   209  	var spec spec
   210  	// Validate -offset, e.g. file.go:#123
   211  	parts := strings.Split(offsetFlag, ":#")
   212  	if len(parts) != 2 {
   213  		return nil, fmt.Errorf("-offset %q: invalid offset specification", offsetFlag)
   214  	}
   215  
   216  	spec.filename = parts[0]
   217  	if !buildutil.FileExists(ctxt, spec.filename) {
   218  		return nil, fmt.Errorf("no such file: %s", spec.filename)
   219  	}
   220  
   221  	bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  	spec.pkg = bp.ImportPath
   226  
   227  	for _, r := range parts[1] {
   228  		if !isDigit(r) {
   229  			return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag)
   230  		}
   231  	}
   232  	spec.offset, err = strconv.Atoi(parts[1])
   233  	if err != nil {
   234  		return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag)
   235  	}
   236  
   237  	// Parse the file and check there's an identifier at that offset.
   238  	fset := token.NewFileSet()
   239  	f, err := buildutil.ParseFile(fset, ctxt, nil, wd, spec.filename, parser.ParseComments)
   240  	if err != nil {
   241  		return nil, fmt.Errorf("-offset %q: cannot parse file: %s", offsetFlag, err)
   242  	}
   243  
   244  	id := identAtOffset(fset, f, spec.offset)
   245  	if id == nil {
   246  		return nil, fmt.Errorf("-offset %q: no identifier at this position", offsetFlag)
   247  	}
   248  
   249  	spec.fromName = id.Name
   250  
   251  	return &spec, nil
   252  }
   253  
   254  var wd = func() string {
   255  	wd, err := os.Getwd()
   256  	if err != nil {
   257  		panic("cannot get working directory: " + err.Error())
   258  	}
   259  	return wd
   260  }()
   261  
   262  // For source trees built with 'go build', the -from or -offset
   263  // spec identifies exactly one initial 'from' object to rename ,
   264  // but certain proprietary build systems allow a single file to
   265  // appear in multiple packages (e.g. the test package contains a
   266  // copy of its library), so there may be multiple objects for
   267  // the same source entity.
   268  
   269  func findFromObjects(iprog *loader.Program, spec *spec) ([]types.Object, error) {
   270  	if spec.filename != "" {
   271  		return findFromObjectsInFile(iprog, spec)
   272  	}
   273  
   274  	// Search for objects defined in specified package.
   275  
   276  	// TODO(adonovan): the iprog.ImportMap has an entry {"main": ...}
   277  	// for main packages, even though that's not an import path.
   278  	// Seems like a bug.
   279  	//
   280  	// pkg := iprog.ImportMap[spec.pkg]
   281  	// if pkg == nil {
   282  	// 	return fmt.Errorf("cannot find package %s", spec.pkg) // can't happen?
   283  	// }
   284  	// info := iprog.AllPackages[pkg]
   285  
   286  	// Workaround: lookup by value.
   287  	var info *loader.PackageInfo
   288  	var pkg *types.Package
   289  	for pkg, info = range iprog.AllPackages {
   290  		if pkg.Path() == spec.pkg {
   291  			break
   292  		}
   293  	}
   294  	if info == nil {
   295  		return nil, fmt.Errorf("package %q was not loaded", spec.pkg)
   296  	}
   297  
   298  	objects, err := findObjects(info, spec)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  	if len(objects) > 1 {
   303  		// ambiguous "*" scope query
   304  		return nil, ambiguityError(iprog.Fset, objects)
   305  	}
   306  	return objects, nil
   307  }
   308  
   309  func findFromObjectsInFile(iprog *loader.Program, spec *spec) ([]types.Object, error) {
   310  	var fromObjects []types.Object
   311  	for _, info := range iprog.AllPackages {
   312  		// restrict to specified filename
   313  		// NB: under certain proprietary build systems, a given
   314  		// filename may appear in multiple packages.
   315  		for _, f := range info.Files {
   316  			thisFile := iprog.Fset.File(f.Pos())
   317  			if !sameFile(thisFile.Name(), spec.filename) {
   318  				continue
   319  			}
   320  			// This package contains the query file.
   321  
   322  			if spec.offset != 0 {
   323  				// We cannot refactor generated files since position information is invalidated.
   324  				if generated(f, thisFile) {
   325  					return nil, fmt.Errorf("cannot rename identifiers in generated file containing DO NOT EDIT marker: %s", thisFile.Name())
   326  				}
   327  
   328  				// Search for a specific ident by file/offset.
   329  				id := identAtOffset(iprog.Fset, f, spec.offset)
   330  				if id == nil {
   331  					// can't happen?
   332  					return nil, fmt.Errorf("identifier not found")
   333  				}
   334  				obj := info.Uses[id]
   335  				if obj == nil {
   336  					obj = info.Defs[id]
   337  					if obj == nil {
   338  						// Ident without Object.
   339  
   340  						// Package clause?
   341  						pos := thisFile.Pos(spec.offset)
   342  						_, path, _ := iprog.PathEnclosingInterval(pos, pos)
   343  						if len(path) == 2 { // [Ident File]
   344  							// TODO(adonovan): support this case.
   345  							return nil, fmt.Errorf("cannot rename %q: renaming package clauses is not yet supported",
   346  								path[1].(*ast.File).Name.Name)
   347  						}
   348  
   349  						// Implicit y in "switch y := x.(type) {"?
   350  						if obj := typeSwitchVar(&info.Info, path); obj != nil {
   351  							return []types.Object{obj}, nil
   352  						}
   353  
   354  						// Probably a type error.
   355  						return nil, fmt.Errorf("cannot find object for %q", id.Name)
   356  					}
   357  				}
   358  				if obj.Pkg() == nil {
   359  					return nil, fmt.Errorf("cannot rename predeclared identifiers (%s)", obj)
   360  
   361  				}
   362  
   363  				fromObjects = append(fromObjects, obj)
   364  			} else {
   365  				// do a package-wide query
   366  				objects, err := findObjects(info, spec)
   367  				if err != nil {
   368  					return nil, err
   369  				}
   370  
   371  				// filter results: only objects defined in thisFile
   372  				var filtered []types.Object
   373  				for _, obj := range objects {
   374  					if iprog.Fset.File(obj.Pos()) == thisFile {
   375  						filtered = append(filtered, obj)
   376  					}
   377  				}
   378  				if len(filtered) == 0 {
   379  					return nil, fmt.Errorf("no object %q declared in file %s",
   380  						spec.fromName, spec.filename)
   381  				} else if len(filtered) > 1 {
   382  					return nil, ambiguityError(iprog.Fset, filtered)
   383  				}
   384  				fromObjects = append(fromObjects, filtered[0])
   385  			}
   386  			break
   387  		}
   388  	}
   389  	if len(fromObjects) == 0 {
   390  		// can't happen?
   391  		return nil, fmt.Errorf("file %s was not part of the loaded program", spec.filename)
   392  	}
   393  	return fromObjects, nil
   394  }
   395  
   396  func typeSwitchVar(info *types.Info, path []ast.Node) types.Object {
   397  	if len(path) > 3 {
   398  		// [Ident AssignStmt TypeSwitchStmt...]
   399  		if sw, ok := path[2].(*ast.TypeSwitchStmt); ok {
   400  			// choose the first case.
   401  			if len(sw.Body.List) > 0 {
   402  				obj := info.Implicits[sw.Body.List[0].(*ast.CaseClause)]
   403  				if obj != nil {
   404  					return obj
   405  				}
   406  			}
   407  		}
   408  	}
   409  	return nil
   410  }
   411  
   412  // On success, findObjects returns the list of objects named
   413  // spec.fromName matching the spec.  On success, the result has exactly
   414  // one element unless spec.searchFor!="", in which case it has at least one
   415  // element.
   416  //
   417  func findObjects(info *loader.PackageInfo, spec *spec) ([]types.Object, error) {
   418  	if spec.pkgMember == "" {
   419  		if spec.searchFor == "" {
   420  			panic(spec)
   421  		}
   422  		objects := searchDefs(&info.Info, spec.searchFor)
   423  		if objects == nil {
   424  			return nil, fmt.Errorf("no object %q declared in package %q",
   425  				spec.searchFor, info.Pkg.Path())
   426  		}
   427  		return objects, nil
   428  	}
   429  
   430  	pkgMember := info.Pkg.Scope().Lookup(spec.pkgMember)
   431  	if pkgMember == nil {
   432  		return nil, fmt.Errorf("package %q has no member %q",
   433  			info.Pkg.Path(), spec.pkgMember)
   434  	}
   435  
   436  	var searchFunc *types.Func
   437  	if spec.typeMember == "" {
   438  		// package member
   439  		if spec.searchFor == "" {
   440  			return []types.Object{pkgMember}, nil
   441  		}
   442  
   443  		// Search within pkgMember, which must be a function.
   444  		searchFunc, _ = pkgMember.(*types.Func)
   445  		if searchFunc == nil {
   446  			return nil, fmt.Errorf("cannot search for %q within %s %q",
   447  				spec.searchFor, objectKind(pkgMember), pkgMember)
   448  		}
   449  	} else {
   450  		// field/method of type
   451  		// e.g. (encoding/json.Decoder).Decode
   452  		// or ::x within it.
   453  
   454  		tName, _ := pkgMember.(*types.TypeName)
   455  		if tName == nil {
   456  			return nil, fmt.Errorf("%s.%s is a %s, not a type",
   457  				info.Pkg.Path(), pkgMember.Name(), objectKind(pkgMember))
   458  		}
   459  
   460  		// search within named type.
   461  		obj, _, _ := types.LookupFieldOrMethod(tName.Type(), true, info.Pkg, spec.typeMember)
   462  		if obj == nil {
   463  			return nil, fmt.Errorf("cannot find field or method %q of %s %s.%s",
   464  				spec.typeMember, typeKind(tName.Type()), info.Pkg.Path(), tName.Name())
   465  		}
   466  
   467  		if spec.searchFor == "" {
   468  			// If it is an embedded field, return the type of the field.
   469  			if v, ok := obj.(*types.Var); ok && v.Anonymous() {
   470  				switch t := v.Type().(type) {
   471  				case *types.Pointer:
   472  					return []types.Object{t.Elem().(*types.Named).Obj()}, nil
   473  				case *types.Named:
   474  					return []types.Object{t.Obj()}, nil
   475  				}
   476  			}
   477  			return []types.Object{obj}, nil
   478  		}
   479  
   480  		searchFunc, _ = obj.(*types.Func)
   481  		if searchFunc == nil {
   482  			return nil, fmt.Errorf("cannot search for local name %q within %s (%s.%s).%s; need a function",
   483  				spec.searchFor, objectKind(obj), info.Pkg.Path(), tName.Name(),
   484  				obj.Name())
   485  		}
   486  		if isInterface(tName.Type()) {
   487  			return nil, fmt.Errorf("cannot search for local name %q within abstract method (%s.%s).%s",
   488  				spec.searchFor, info.Pkg.Path(), tName.Name(), searchFunc.Name())
   489  		}
   490  	}
   491  
   492  	// -- search within function or method --
   493  
   494  	decl := funcDecl(info, searchFunc)
   495  	if decl == nil {
   496  		return nil, fmt.Errorf("cannot find syntax for %s", searchFunc) // can't happen?
   497  	}
   498  
   499  	var objects []types.Object
   500  	for _, obj := range searchDefs(&info.Info, spec.searchFor) {
   501  		// We use positions, not scopes, to determine whether
   502  		// the obj is within searchFunc.  This is clumsy, but the
   503  		// alternative, using the types.Scope tree, doesn't
   504  		// account for non-lexical objects like fields and
   505  		// interface methods.
   506  		if decl.Pos() <= obj.Pos() && obj.Pos() < decl.End() && obj != searchFunc {
   507  			objects = append(objects, obj)
   508  		}
   509  	}
   510  	if objects == nil {
   511  		return nil, fmt.Errorf("no local definition of %q within %s",
   512  			spec.searchFor, searchFunc)
   513  	}
   514  	return objects, nil
   515  }
   516  
   517  func funcDecl(info *loader.PackageInfo, fn *types.Func) *ast.FuncDecl {
   518  	for _, f := range info.Files {
   519  		for _, d := range f.Decls {
   520  			if d, ok := d.(*ast.FuncDecl); ok && info.Defs[d.Name] == fn {
   521  				return d
   522  			}
   523  		}
   524  	}
   525  	return nil
   526  }
   527  
   528  func searchDefs(info *types.Info, name string) []types.Object {
   529  	var objects []types.Object
   530  	for id, obj := range info.Defs {
   531  		if obj == nil {
   532  			// e.g. blank ident.
   533  			// TODO(adonovan): but also implicit y in
   534  			//    switch y := x.(type)
   535  			// Needs some thought.
   536  			continue
   537  		}
   538  		if id.Name == name {
   539  			objects = append(objects, obj)
   540  		}
   541  	}
   542  	return objects
   543  }
   544  
   545  func identAtOffset(fset *token.FileSet, f *ast.File, offset int) *ast.Ident {
   546  	var found *ast.Ident
   547  	ast.Inspect(f, func(n ast.Node) bool {
   548  		if id, ok := n.(*ast.Ident); ok {
   549  			idpos := fset.Position(id.Pos()).Offset
   550  			if idpos <= offset && offset < idpos+len(id.Name) {
   551  				found = id
   552  			}
   553  		}
   554  		return found == nil // keep traversing only until found
   555  	})
   556  	return found
   557  }
   558  
   559  // ambiguityError returns an error describing an ambiguous "*" scope query.
   560  func ambiguityError(fset *token.FileSet, objects []types.Object) error {
   561  	var buf bytes.Buffer
   562  	for i, obj := range objects {
   563  		if i > 0 {
   564  			buf.WriteString(", ")
   565  		}
   566  		posn := fset.Position(obj.Pos())
   567  		fmt.Fprintf(&buf, "%s at %s:%d:%d",
   568  			objectKind(obj), filepath.Base(posn.Filename), posn.Line, posn.Column)
   569  	}
   570  	return fmt.Errorf("ambiguous specifier %s matches %s",
   571  		objects[0].Name(), buf.String())
   572  }
   573  
   574  // Matches cgo generated comment as well as the proposed standard:
   575  //	https://golang.org/s/generatedcode
   576  var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
   577  
   578  // generated reports whether ast.File is a generated file.
   579  func generated(f *ast.File, tokenFile *token.File) bool {
   580  
   581  	// Iterate over the comments in the file
   582  	for _, commentGroup := range f.Comments {
   583  		for _, comment := range commentGroup.List {
   584  			if matched := generatedRx.MatchString(comment.Text); matched {
   585  				// Check if comment is at the beginning of the line in source
   586  				if pos := tokenFile.Position(comment.Slash); pos.Column == 1 {
   587  					return true
   588  				}
   589  			}
   590  		}
   591  	}
   592  	return false
   593  }