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
116func (r *Reader) getModuleTypeDoc(pkgPath, factoryFuncName string) (string, error) {
117	goPkg, err := r.goPkg(pkgPath)
118	if err != nil {
119		return "", err
120	}
121
122	for _, fn := range goPkg.Funcs {
123		if fn.Name == factoryFuncName {
124			return fn.Doc, nil
125		}
126	}
127
128	// The doc package may associate the method with the type it returns, so iterate through those too
129	for _, typ := range goPkg.Types {
130		for _, fn := range typ.Funcs {
131			if fn.Name == factoryFuncName {
132				return fn.Doc, nil
133			}
134		}
135	}
136
137	return "", nil
138}
139
140func (r *Reader) getPropertyStruct(pkgPath, name string) *PropertyStruct {
141	r.mutex.Lock()
142	defer r.mutex.Unlock()
143
144	name = pkgPath + "." + name
145
146	return r.ps[name]
147}
148
149func (r *Reader) putPropertyStruct(pkgPath, name string, ps *PropertyStruct) *PropertyStruct {
150	r.mutex.Lock()
151	defer r.mutex.Unlock()
152
153	name = pkgPath + "." + name
154
155	if r.ps[name] != nil {
156		return r.ps[name]
157	} else {
158		r.ps[name] = ps
159		return ps
160	}
161}
162
163// Package AST generation and storage
164func (r *Reader) goPkg(pkgPath string) (*doc.Package, error) {
165	pkg := r.getGoPkg(pkgPath)
166	if pkg == nil {
167		if files, ok := r.pkgFiles[pkgPath]; ok {
168			var err error
169			pkgAST, err := packageAST(files)
170			if err != nil {
171				return nil, err
172			}
173			pkg = doc.New(pkgAST, pkgPath, doc.AllDecls)
174			pkg = r.putGoPkg(pkgPath, pkg)
175		} else {
176			return nil, fmt.Errorf("unknown package %q", pkgPath)
177		}
178	}
179	return pkg, nil
180}
181
182func (r *Reader) getGoPkg(pkgPath string) *doc.Package {
183	r.mutex.Lock()
184	defer r.mutex.Unlock()
185
186	return r.goPkgs[pkgPath]
187}
188
189func (r *Reader) putGoPkg(pkgPath string, pkg *doc.Package) *doc.Package {
190	r.mutex.Lock()
191	defer r.mutex.Unlock()
192
193	if r.goPkgs[pkgPath] != nil {
194		return r.goPkgs[pkgPath]
195	} else {
196		r.goPkgs[pkgPath] = pkg
197		return pkg
198	}
199}
200
201// A regex to find a package path within a function name. It finds the shortest string that is
202// followed by '.' and doesn't have any '/'s left.
203var pkgPathRe = regexp.MustCompile("^(.*?)\\.[^/]+$")
204
205func funcNameToPkgPath(f string) (string, error) {
206	s := pkgPathRe.FindStringSubmatch(f)
207	if len(s) < 2 {
208		return "", fmt.Errorf("failed to extract package path from %q", f)
209	}
210	return s[1], nil
211}
212
213func packageAST(files []string) (*ast.Package, error) {
214	asts := make(map[string]*ast.File)
215
216	fset := token.NewFileSet()
217	for _, file := range files {
218		ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
219		if err != nil {
220			return nil, err
221		}
222		asts[file] = ast
223	}
224
225	pkg, _ := ast.NewPackage(fset, asts, nil, nil)
226	return pkg, nil
227}
228