github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/command/commandmeta/meta.go (about)

     1  // Copyright 2021 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 commandmeta provides metadata about LSP commands, by analyzing the
     6  // command.Interface type.
     7  package commandmeta
     8  
     9  import (
    10  	"fmt"
    11  	"go/ast"
    12  	"go/token"
    13  	"go/types"
    14  	"reflect"
    15  	"strings"
    16  	"unicode"
    17  
    18  	"github.com/powerman/golang-tools/go/ast/astutil"
    19  	"github.com/powerman/golang-tools/go/packages"
    20  	"github.com/powerman/golang-tools/internal/lsp/command"
    21  )
    22  
    23  type Command struct {
    24  	MethodName string
    25  	Name       string
    26  	// TODO(rFindley): I think Title can actually be eliminated. In all cases
    27  	// where we use it, there is probably a more appropriate contextual title.
    28  	Title  string
    29  	Doc    string
    30  	Args   []*Field
    31  	Result *Field
    32  }
    33  
    34  func (c *Command) ID() string {
    35  	return command.ID(c.Name)
    36  }
    37  
    38  type Field struct {
    39  	Name     string
    40  	Doc      string
    41  	JSONTag  string
    42  	Type     types.Type
    43  	FieldMod string
    44  	// In some circumstances, we may want to recursively load additional field
    45  	// descriptors for fields of struct types, documenting their internals.
    46  	Fields []*Field
    47  }
    48  
    49  func Load() (*packages.Package, []*Command, error) {
    50  	pkgs, err := packages.Load(
    51  		&packages.Config{
    52  			Mode:       packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
    53  			BuildFlags: []string{"-tags=generate"},
    54  		},
    55  		"github.com/powerman/golang-tools/internal/lsp/command",
    56  	)
    57  	if err != nil {
    58  		return nil, nil, fmt.Errorf("packages.Load: %v", err)
    59  	}
    60  	pkg := pkgs[0]
    61  	if len(pkg.Errors) > 0 {
    62  		return pkg, nil, pkg.Errors[0]
    63  	}
    64  
    65  	// For a bit of type safety, use reflection to get the interface name within
    66  	// the package scope.
    67  	it := reflect.TypeOf((*command.Interface)(nil)).Elem()
    68  	obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface)
    69  
    70  	// Load command metadata corresponding to each interface method.
    71  	var commands []*Command
    72  	loader := fieldLoader{make(map[types.Object]*Field)}
    73  	for i := 0; i < obj.NumMethods(); i++ {
    74  		m := obj.Method(i)
    75  		c, err := loader.loadMethod(pkg, m)
    76  		if err != nil {
    77  			return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err)
    78  		}
    79  		commands = append(commands, c)
    80  	}
    81  	return pkg, commands, nil
    82  }
    83  
    84  // fieldLoader loads field information, memoizing results to prevent infinite
    85  // recursion.
    86  type fieldLoader struct {
    87  	loaded map[types.Object]*Field
    88  }
    89  
    90  var universeError = types.Universe.Lookup("error").Type()
    91  
    92  func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) {
    93  	node, err := findField(pkg, m.Pos())
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	title, doc := splitDoc(node.Doc.Text())
    98  	c := &Command{
    99  		MethodName: m.Name(),
   100  		Name:       lspName(m.Name()),
   101  		Doc:        doc,
   102  		Title:      title,
   103  	}
   104  	sig := m.Type().Underlying().(*types.Signature)
   105  	rlen := sig.Results().Len()
   106  	if rlen > 2 || rlen == 0 {
   107  		return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen)
   108  	}
   109  	finalResult := sig.Results().At(rlen - 1)
   110  	if !types.Identical(finalResult.Type(), universeError) {
   111  		return nil, fmt.Errorf("final return must be error")
   112  	}
   113  	if rlen == 2 {
   114  		obj := sig.Results().At(0)
   115  		c.Result, err = l.loadField(pkg, obj, "", "")
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  	}
   120  	for i := 0; i < sig.Params().Len(); i++ {
   121  		obj := sig.Params().At(i)
   122  		fld, err := l.loadField(pkg, obj, "", "")
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  		if i == 0 {
   127  			// Lazy check that the first argument is a context. We could relax this,
   128  			// but then the generated code gets more complicated.
   129  			if named, ok := fld.Type.(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" {
   130  				return nil, fmt.Errorf("first method parameter must be context.Context")
   131  			}
   132  			// Skip the context argument, as it is implied.
   133  			continue
   134  		}
   135  		c.Args = append(c.Args, fld)
   136  	}
   137  	return c, nil
   138  }
   139  
   140  func (l *fieldLoader) loadField(pkg *packages.Package, obj *types.Var, doc, tag string) (*Field, error) {
   141  	if existing, ok := l.loaded[obj]; ok {
   142  		return existing, nil
   143  	}
   144  	fld := &Field{
   145  		Name:    obj.Name(),
   146  		Doc:     strings.TrimSpace(doc),
   147  		Type:    obj.Type(),
   148  		JSONTag: reflect.StructTag(tag).Get("json"),
   149  	}
   150  	under := fld.Type.Underlying()
   151  	// Quick-and-dirty handling for various underlying types.
   152  	switch p := under.(type) {
   153  	case *types.Pointer:
   154  		under = p.Elem().Underlying()
   155  	case *types.Array:
   156  		under = p.Elem().Underlying()
   157  		fld.FieldMod = fmt.Sprintf("[%d]", p.Len())
   158  	case *types.Slice:
   159  		under = p.Elem().Underlying()
   160  		fld.FieldMod = "[]"
   161  	}
   162  
   163  	if s, ok := under.(*types.Struct); ok {
   164  		for i := 0; i < s.NumFields(); i++ {
   165  			obj2 := s.Field(i)
   166  			pkg2 := pkg
   167  			if obj2.Pkg() != pkg2.Types {
   168  				pkg2, ok = pkg.Imports[obj2.Pkg().Path()]
   169  				if !ok {
   170  					return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path())
   171  				}
   172  			}
   173  			node, err := findField(pkg2, obj2.Pos())
   174  			if err != nil {
   175  				return nil, err
   176  			}
   177  			tag := s.Tag(i)
   178  			structField, err := l.loadField(pkg2, obj2, node.Doc.Text(), tag)
   179  			if err != nil {
   180  				return nil, err
   181  			}
   182  			fld.Fields = append(fld.Fields, structField)
   183  		}
   184  	}
   185  	return fld, nil
   186  }
   187  
   188  // splitDoc parses a command doc string to separate the title from normal
   189  // documentation.
   190  //
   191  // The doc comment should be of the form: "MethodName: Title\nDocumentation"
   192  func splitDoc(text string) (title, doc string) {
   193  	docParts := strings.SplitN(text, "\n", 2)
   194  	titleParts := strings.SplitN(docParts[0], ":", 2)
   195  	if len(titleParts) > 1 {
   196  		title = strings.TrimSpace(titleParts[1])
   197  	}
   198  	if len(docParts) > 1 {
   199  		doc = strings.TrimSpace(docParts[1])
   200  	}
   201  	return title, doc
   202  }
   203  
   204  // lspName returns the normalized command name to use in the LSP.
   205  func lspName(methodName string) string {
   206  	words := splitCamel(methodName)
   207  	for i := range words {
   208  		words[i] = strings.ToLower(words[i])
   209  	}
   210  	return strings.Join(words, "_")
   211  }
   212  
   213  // splitCamel splits s into words, according to camel-case word boundaries.
   214  // Initialisms are grouped as a single word.
   215  //
   216  // For example:
   217  //  "RunTests" -> []string{"Run", "Tests"}
   218  //  "GCDetails" -> []string{"GC", "Details"}
   219  func splitCamel(s string) []string {
   220  	var words []string
   221  	for len(s) > 0 {
   222  		last := strings.LastIndexFunc(s, unicode.IsUpper)
   223  		if last < 0 {
   224  			last = 0
   225  		}
   226  		if last == len(s)-1 {
   227  			// Group initialisms as a single word.
   228  			last = 1 + strings.LastIndexFunc(s[:last], func(r rune) bool { return !unicode.IsUpper(r) })
   229  		}
   230  		words = append(words, s[last:])
   231  		s = s[:last]
   232  	}
   233  	for i := 0; i < len(words)/2; i++ {
   234  		j := len(words) - i - 1
   235  		words[i], words[j] = words[j], words[i]
   236  	}
   237  	return words
   238  }
   239  
   240  // findField finds the struct field or interface method positioned at pos,
   241  // within the AST.
   242  func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) {
   243  	fset := pkg.Fset
   244  	var file *ast.File
   245  	for _, f := range pkg.Syntax {
   246  		if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
   247  			file = f
   248  			break
   249  		}
   250  	}
   251  	if file == nil {
   252  		return nil, fmt.Errorf("no file for pos %v", pos)
   253  	}
   254  	path, _ := astutil.PathEnclosingInterval(file, pos, pos)
   255  	// This is fragile, but in the cases we care about, the field will be in
   256  	// path[1].
   257  	return path[1].(*ast.Field), nil
   258  }