• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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