blob: a39ee3c290c45ce4a9b92318eb95fbc016b85ee3 [file] [log] [blame]
Jaewoong Jung781f6b22019-02-06 16:20:17 -08001// 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,
Jaewoong Jung8bc6bf12019-03-11 10:54:36 -070080 Text: formatText(text),
Jaewoong Jung781f6b22019-02-06 16:20:17 -080081 }, 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}