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