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 }