// 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 . 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 }