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