1// Copyright 2019 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package bpdoc 16 17import ( 18 "fmt" 19 "go/ast" 20 "go/doc" 21 "go/parser" 22 "go/token" 23 "reflect" 24 "regexp" 25 "runtime" 26 "strings" 27 "sync" 28) 29 30// Handles parsing and low-level processing of Blueprint module source files. Note that most getter 31// functions associated with Reader only fill basic information that can be simply extracted from 32// AST parsing results. More sophisticated processing is performed in bpdoc.go 33type Reader struct { 34 pkgFiles map[string][]string // Map of package name to source files, provided by constructor 35 36 mutex sync.Mutex 37 goPkgs map[string]*doc.Package // Map of package name to parsed Go AST, protected by mutex 38 ps map[string]*PropertyStruct // Map of module type name to property struct, protected by mutex 39} 40 41func NewReader(pkgFiles map[string][]string) *Reader { 42 return &Reader{ 43 pkgFiles: pkgFiles, 44 goPkgs: make(map[string]*doc.Package), 45 ps: make(map[string]*PropertyStruct), 46 } 47} 48 49func (r *Reader) Package(path string) (*Package, error) { 50 goPkg, err := r.goPkg(path) 51 if err != nil { 52 return nil, err 53 } 54 55 return &Package{ 56 Name: goPkg.Name, 57 Path: path, 58 Text: goPkg.Doc, 59 }, nil 60} 61 62func (r *Reader) ModuleType(name string, factory reflect.Value) (*ModuleType, error) { 63 f := runtime.FuncForPC(factory.Pointer()) 64 65 pkgPath, err := funcNameToPkgPath(f.Name()) 66 if err != nil { 67 return nil, err 68 } 69 70 factoryName := strings.TrimPrefix(f.Name(), pkgPath+".") 71 72 text, err := r.getModuleTypeDoc(pkgPath, factoryName) 73 if err != nil { 74 return nil, err 75 } 76 77 return &ModuleType{ 78 Name: name, 79 PkgPath: pkgPath, 80 Text: formatText(text), 81 }, nil 82} 83 84// Return the PropertyStruct associated with a property struct type. The type should be in the 85// format <package path>.<type name> 86func (r *Reader) propertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) { 87 ps := r.getPropertyStruct(pkgPath, name) 88 89 if ps == nil { 90 pkg, err := r.goPkg(pkgPath) 91 if err != nil { 92 return nil, err 93 } 94 95 for _, t := range pkg.Types { 96 if t.Name == name { 97 ps, err = newPropertyStruct(t) 98 if err != nil { 99 return nil, err 100 } 101 ps = r.putPropertyStruct(pkgPath, name, ps) 102 } 103 } 104 } 105 106 if ps == nil { 107 return nil, fmt.Errorf("package %q type %q not found", pkgPath, name) 108 } 109 110 ps = ps.Clone() 111 ps.SetDefaults(defaults) 112 113 return ps, nil 114} 115 116// Return the PropertyStruct associated with a struct type using recursion 117// This method is useful since golang structs created using reflection have an empty PkgPath() 118func (r *Reader) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) { 119 var props []Property 120 121 // Base case: primitive type 122 if defaults.Kind() != reflect.Struct { 123 props = append(props, Property{Name: name, 124 Type: defaults.Type().String()}) 125 return &PropertyStruct{Properties: props}, nil 126 } 127 128 // Base case: use r.propertyStruct if struct has a non empty pkgpath 129 if pkgPath != "" { 130 return r.propertyStruct(pkgPath, name, defaults) 131 } 132 133 numFields := defaults.NumField() 134 for i := 0; i < numFields; i++ { 135 field := defaults.Type().Field(i) 136 // Recurse 137 ps, err := r.PropertyStruct(field.Type.PkgPath(), field.Type.Name(), reflect.New(field.Type).Elem()) 138 139 if err != nil { 140 return nil, err 141 } 142 prop := Property{ 143 Name: strings.ToLower(field.Name), 144 Text: formatText(ps.Text), 145 Type: field.Type.Name(), 146 Properties: ps.Properties, 147 } 148 props = append(props, prop) 149 } 150 return &PropertyStruct{Properties: props}, nil 151} 152 153func (r *Reader) getModuleTypeDoc(pkgPath, factoryFuncName string) (string, error) { 154 goPkg, err := r.goPkg(pkgPath) 155 if err != nil { 156 return "", err 157 } 158 159 for _, fn := range goPkg.Funcs { 160 if fn.Name == factoryFuncName { 161 return fn.Doc, nil 162 } 163 } 164 165 // The doc package may associate the method with the type it returns, so iterate through those too 166 for _, typ := range goPkg.Types { 167 for _, fn := range typ.Funcs { 168 if fn.Name == factoryFuncName { 169 return fn.Doc, nil 170 } 171 } 172 } 173 174 return "", nil 175} 176 177func (r *Reader) getPropertyStruct(pkgPath, name string) *PropertyStruct { 178 r.mutex.Lock() 179 defer r.mutex.Unlock() 180 181 name = pkgPath + "." + name 182 183 return r.ps[name] 184} 185 186func (r *Reader) putPropertyStruct(pkgPath, name string, ps *PropertyStruct) *PropertyStruct { 187 r.mutex.Lock() 188 defer r.mutex.Unlock() 189 190 name = pkgPath + "." + name 191 192 if r.ps[name] != nil { 193 return r.ps[name] 194 } else { 195 r.ps[name] = ps 196 return ps 197 } 198} 199 200// Package AST generation and storage 201func (r *Reader) goPkg(pkgPath string) (*doc.Package, error) { 202 pkg := r.getGoPkg(pkgPath) 203 if pkg == nil { 204 if files, ok := r.pkgFiles[pkgPath]; ok { 205 var err error 206 pkgAST, err := packageAST(files) 207 if err != nil { 208 return nil, err 209 } 210 pkg = doc.New(pkgAST, pkgPath, doc.AllDecls) 211 pkg = r.putGoPkg(pkgPath, pkg) 212 } else { 213 return nil, fmt.Errorf("unknown package %q", pkgPath) 214 } 215 } 216 return pkg, nil 217} 218 219func (r *Reader) getGoPkg(pkgPath string) *doc.Package { 220 r.mutex.Lock() 221 defer r.mutex.Unlock() 222 223 return r.goPkgs[pkgPath] 224} 225 226func (r *Reader) putGoPkg(pkgPath string, pkg *doc.Package) *doc.Package { 227 r.mutex.Lock() 228 defer r.mutex.Unlock() 229 230 if r.goPkgs[pkgPath] != nil { 231 return r.goPkgs[pkgPath] 232 } else { 233 r.goPkgs[pkgPath] = pkg 234 return pkg 235 } 236} 237 238// A regex to find a package path within a function name. It finds the shortest string that is 239// followed by '.' and doesn't have any '/'s left. 240var pkgPathRe = regexp.MustCompile("^(.*?)\\.[^/]+$") 241 242func funcNameToPkgPath(f string) (string, error) { 243 s := pkgPathRe.FindStringSubmatch(f) 244 if len(s) < 2 { 245 return "", fmt.Errorf("failed to extract package path from %q", f) 246 } 247 return s[1], nil 248} 249 250func packageAST(files []string) (*ast.Package, error) { 251 asts := make(map[string]*ast.File) 252 253 fset := token.NewFileSet() 254 for _, file := range files { 255 ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments) 256 if err != nil { 257 return nil, err 258 } 259 asts[file] = ast 260 } 261 262 pkg, _ := ast.NewPackage(fset, asts, nil, nil) 263 return pkg, nil 264} 265