| // Copyright 2019 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package bpdoc |
| |
| import ( |
| "fmt" |
| "go/ast" |
| "go/doc" |
| "go/parser" |
| "go/token" |
| "reflect" |
| "regexp" |
| "runtime" |
| "strings" |
| "sync" |
| ) |
| |
| // Handles parsing and low-level processing of Blueprint module source files. Note that most getter |
| // functions associated with Reader only fill basic information that can be simply extracted from |
| // AST parsing results. More sophisticated processing is performed in bpdoc.go |
| type Reader struct { |
| pkgFiles map[string][]string // Map of package name to source files, provided by constructor |
| |
| mutex sync.Mutex |
| goPkgs map[string]*doc.Package // Map of package name to parsed Go AST, protected by mutex |
| ps map[string]*PropertyStruct // Map of module type name to property struct, protected by mutex |
| } |
| |
| func NewReader(pkgFiles map[string][]string) *Reader { |
| return &Reader{ |
| pkgFiles: pkgFiles, |
| goPkgs: make(map[string]*doc.Package), |
| ps: make(map[string]*PropertyStruct), |
| } |
| } |
| |
| func (r *Reader) Package(path string) (*Package, error) { |
| goPkg, err := r.goPkg(path) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &Package{ |
| Name: goPkg.Name, |
| Path: path, |
| Text: goPkg.Doc, |
| }, nil |
| } |
| |
| func (r *Reader) ModuleType(name string, factory reflect.Value) (*ModuleType, error) { |
| f := runtime.FuncForPC(factory.Pointer()) |
| |
| pkgPath, err := funcNameToPkgPath(f.Name()) |
| if err != nil { |
| return nil, err |
| } |
| |
| factoryName := strings.TrimPrefix(f.Name(), pkgPath+".") |
| |
| text, err := r.getModuleTypeDoc(pkgPath, factoryName) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &ModuleType{ |
| Name: name, |
| PkgPath: pkgPath, |
| Text: formatText(text), |
| }, nil |
| } |
| |
| // Return the PropertyStruct associated with a property struct type. The type should be in the |
| // format <package path>.<type name> |
| func (r *Reader) propertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) { |
| ps := r.getPropertyStruct(pkgPath, name) |
| |
| if ps == nil { |
| pkg, err := r.goPkg(pkgPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, t := range pkg.Types { |
| if t.Name == name { |
| ps, err = newPropertyStruct(t) |
| if err != nil { |
| return nil, err |
| } |
| ps = r.putPropertyStruct(pkgPath, name, ps) |
| } |
| } |
| } |
| |
| if ps == nil { |
| return nil, fmt.Errorf("package %q type %q not found", pkgPath, name) |
| } |
| |
| ps = ps.Clone() |
| ps.SetDefaults(defaults) |
| |
| return ps, nil |
| } |
| |
| // Return the PropertyStruct associated with a struct type using recursion |
| // This method is useful since golang structs created using reflection have an empty PkgPath() |
| func (r *Reader) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) { |
| var props []Property |
| |
| // Base case: primitive type |
| if defaults.Kind() != reflect.Struct { |
| props = append(props, Property{Name: name, |
| Type: defaults.Type().String()}) |
| return &PropertyStruct{Properties: props}, nil |
| } |
| |
| // Base case: use r.propertyStruct if struct has a non empty pkgpath |
| if pkgPath != "" { |
| return r.propertyStruct(pkgPath, name, defaults) |
| } |
| |
| numFields := defaults.NumField() |
| for i := 0; i < numFields; i++ { |
| field := defaults.Type().Field(i) |
| // Recurse |
| ps, err := r.PropertyStruct(field.Type.PkgPath(), field.Type.Name(), reflect.New(field.Type).Elem()) |
| |
| if err != nil { |
| return nil, err |
| } |
| prop := Property{ |
| Name: strings.ToLower(field.Name), |
| Text: formatText(ps.Text), |
| Type: field.Type.Name(), |
| Properties: ps.Properties, |
| } |
| props = append(props, prop) |
| } |
| return &PropertyStruct{Properties: props}, nil |
| } |
| |
| func (r *Reader) getModuleTypeDoc(pkgPath, factoryFuncName string) (string, error) { |
| goPkg, err := r.goPkg(pkgPath) |
| if err != nil { |
| return "", err |
| } |
| |
| for _, fn := range goPkg.Funcs { |
| if fn.Name == factoryFuncName { |
| return fn.Doc, nil |
| } |
| } |
| |
| // The doc package may associate the method with the type it returns, so iterate through those too |
| for _, typ := range goPkg.Types { |
| for _, fn := range typ.Funcs { |
| if fn.Name == factoryFuncName { |
| return fn.Doc, nil |
| } |
| } |
| } |
| |
| return "", nil |
| } |
| |
| func (r *Reader) getPropertyStruct(pkgPath, name string) *PropertyStruct { |
| r.mutex.Lock() |
| defer r.mutex.Unlock() |
| |
| name = pkgPath + "." + name |
| |
| return r.ps[name] |
| } |
| |
| func (r *Reader) putPropertyStruct(pkgPath, name string, ps *PropertyStruct) *PropertyStruct { |
| r.mutex.Lock() |
| defer r.mutex.Unlock() |
| |
| name = pkgPath + "." + name |
| |
| if r.ps[name] != nil { |
| return r.ps[name] |
| } else { |
| r.ps[name] = ps |
| return ps |
| } |
| } |
| |
| // Package AST generation and storage |
| func (r *Reader) goPkg(pkgPath string) (*doc.Package, error) { |
| pkg := r.getGoPkg(pkgPath) |
| if pkg == nil { |
| if files, ok := r.pkgFiles[pkgPath]; ok { |
| var err error |
| pkgAST, err := packageAST(files) |
| if err != nil { |
| return nil, err |
| } |
| pkg = doc.New(pkgAST, pkgPath, doc.AllDecls) |
| pkg = r.putGoPkg(pkgPath, pkg) |
| } else { |
| return nil, fmt.Errorf("unknown package %q", pkgPath) |
| } |
| } |
| return pkg, nil |
| } |
| |
| func (r *Reader) getGoPkg(pkgPath string) *doc.Package { |
| r.mutex.Lock() |
| defer r.mutex.Unlock() |
| |
| return r.goPkgs[pkgPath] |
| } |
| |
| func (r *Reader) putGoPkg(pkgPath string, pkg *doc.Package) *doc.Package { |
| r.mutex.Lock() |
| defer r.mutex.Unlock() |
| |
| if r.goPkgs[pkgPath] != nil { |
| return r.goPkgs[pkgPath] |
| } else { |
| r.goPkgs[pkgPath] = pkg |
| return pkg |
| } |
| } |
| |
| // A regex to find a package path within a function name. It finds the shortest string that is |
| // followed by '.' and doesn't have any '/'s left. |
| var pkgPathRe = regexp.MustCompile("^(.*?)\\.[^/]+$") |
| |
| func funcNameToPkgPath(f string) (string, error) { |
| s := pkgPathRe.FindStringSubmatch(f) |
| if len(s) < 2 { |
| return "", fmt.Errorf("failed to extract package path from %q", f) |
| } |
| return s[1], nil |
| } |
| |
| func packageAST(files []string) (*ast.Package, error) { |
| asts := make(map[string]*ast.File) |
| |
| fset := token.NewFileSet() |
| for _, file := range files { |
| ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments) |
| if err != nil { |
| return nil, err |
| } |
| asts[file] = ast |
| } |
| |
| pkg, _ := ast.NewPackage(fset, asts, nil, nil) |
| return pkg, nil |
| } |