-
Notifications
You must be signed in to change notification settings - Fork 0
/
analyze.go
182 lines (150 loc) · 5.05 KB
/
analyze.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package tmpl
import (
"context"
"errors"
"fmt"
"html/template"
"reflect"
"strings"
"text/template/parse"
)
type FuncMap = template.FuncMap
// AnalysisHelper is a struct that contains all the data collected
// during an analysis of a TemplateProvider.
//
// An Analysis runs in two passes. The first pass collects important
// contextual information about the template definition tree that can
// be accessed in the second pass. The second pass is the actual analysis
// of the template definition tree where errors and warnings are added.
type AnalysisHelper struct {
ctx context.Context
//pre-analysis data
// treeSet is a map of all templates defined in the TemplateProvider,
// as well as all of its children.
treeSet map[string]*parse.Tree
// fieldTree is a tree structure of all struct fields in the TemplateProvider,
// as well as all of its children.
fieldTree *FieldNode
//analysis data
// errors is a slice of Errors that occurred during analysis.
errors []string
// warnings is a slice of Warnings that occurred during analysis.
warnings []string
// funcMap is a map of functions provided by analyzers that should
// be added before the template is executed.
funcMap template.FuncMap
// TODO: what if...
// Fixers []FixerFn
}
// IsDefinedTemplate returns true if the given template name is defined in the
// analysis target via {{define}}, or defined by any of its embedded templates.
func (h *AnalysisHelper) IsDefinedTemplate(name string) bool {
if name == "outlet" {
return true
}
_, ok := h.treeSet[name]
return ok
}
func (h *AnalysisHelper) GetDefinedField(name string) *FieldNode {
name = strings.TrimPrefix(name, ".")
if len(name) == 0 {
return h.fieldTree
}
return h.fieldTree.FindPath(strings.Split(name, "."))
}
func (h *AnalysisHelper) FuncMap() FuncMap {
return h.funcMap
}
func (h *AnalysisHelper) AddError(node parse.Node, err string) {
// TODO: to get a useful error message, convert byte position (offset) to line numbers
h.errors = append(h.errors, fmt.Sprintf("%v: %s", node.Position(), err))
}
func (h *AnalysisHelper) AddWarning(node parse.Node, err string) {
h.warnings = append(h.warnings, fmt.Sprintf("%v: %s", node.Position(), err))
}
func (h *AnalysisHelper) AddFunc(name string, fn interface{}) {
if h.funcMap == nil {
h.funcMap = make(FuncMap)
}
h.funcMap[name] = fn
}
func (h *AnalysisHelper) Context() context.Context {
return h.ctx
}
func (h *AnalysisHelper) WithContext(ctx context.Context) {
h.ctx = ctx
}
// ParseOptions controls the behavior of the templateProvider parser used by Analyze.
type ParseOptions struct {
Funcs FuncMap
LeftDelim string
RightDelim string
}
type AnalyzerFunc func(val reflect.Value, node parse.Node)
// Analyzer is a type that parses templateProvider text and performs an analysis
type Analyzer func(res *AnalysisHelper) AnalyzerFunc
// Analyze uses reflection on the given TemplateProvider while also parsing the
// templateProvider text to perform an analysis. The analysis is performed by the given
// analyzers. The analysis is returned as an AnalysisHelper struct.
func Analyze(tp TemplateProvider, opts ParseOptions, analyzers []Analyzer) (*AnalysisHelper, error) {
helper, err := createHelper(tp, opts)
if err != nil {
return nil, err
}
pt := helper.treeSet[strings.TrimPrefix(fmt.Sprintf("%T", tp), "*")]
val := reflect.ValueOf(tp)
// Do the actual traversal and analysis of the given template provider
Traverse(pt.Root, Visitor(func(node parse.Node) {
for _, fn := range analyzers {
fn(helper)(val, node)
}
}))
// During runtime compilation we're only worried about errors
// During static analysis we're worried about errors but also
// return the helper to print warnings and other information
if len(helper.errors) > 0 {
errs := make([]error, 0)
for _, err := range helper.errors {
errs = append(errs, fmt.Errorf(err))
}
return helper, errors.Join(errs...)
}
return helper, nil
}
func createHelper(tp TemplateProvider, opts ParseOptions) (helper *AnalysisHelper, err error) {
helper = &AnalysisHelper{
ctx: context.Background(),
treeSet: make(map[string]*parse.Tree),
errors: make([]string, 0),
warnings: make([]string, 0),
funcMap: opts.Funcs,
}
if len(opts.LeftDelim) == 0 || len(opts.RightDelim) == 0 {
opts.LeftDelim = "{{"
opts.RightDelim = "}}"
}
// create a tree of all fields for static type checking
helper.fieldTree, err = createFieldTree(tp)
if err != nil {
return nil, err
}
// create one big parse.Tree set of all templates, including embedded templates
err = recurseFieldsImplementing[TemplateProvider](tp, func(tp TemplateProvider, field reflect.StructField) error {
templateName, ok := field.Tag.Lookup("tmpl")
if !ok {
templateName = strings.TrimPrefix(field.Name, "*")
}
parser := parse.New(templateName)
parser.Mode = parse.SkipFuncCheck | parse.ParseComments
tmp := make(map[string]*parse.Tree)
_, err := parser.Parse(tp.TemplateText(), opts.LeftDelim, opts.RightDelim, tmp, nil)
if err != nil {
return err
}
for k, v := range tmp {
helper.treeSet[k] = v
}
return nil
})
return
}