-
Notifications
You must be signed in to change notification settings - Fork 0
/
bridge.go
223 lines (206 loc) · 6.74 KB
/
bridge.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package superpose
import (
"bytes"
"context"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"strings"
)
type bridgeFile struct {
fileName string
dimPkgRefs dimPkgRefs
}
// May return nil file which means no dimensions referenced
func (s *Superpose) buildBridgeFile(ctx context.Context) (*bridgeFile, error) {
// Get dimensions from every file
builder := &bridgeFileBuilder{
bridgeFile: bridgeFile{dimPkgRefs: dimPkgRefs{}},
imports: map[string]string{},
}
for goFile := range s.flags.goFileIndexes {
if ok, err := s.buildInitStatements(ctx, builder, goFile); err != nil {
return nil, fmt.Errorf("failed building init statements for file %v: %w", goFile, err)
} else if !ok {
// Bail because of parsing issues
return nil, nil
}
}
// If there were no references, no bridge file
if len(builder.dimPkgRefs) == 0 {
return nil, nil
}
// Build code for the file. We accept neither the import order nor the init
// statement order is deterministic.
code := "package " + builder.pkgName + "\n\n"
for importPath, alias := range builder.imports {
code += fmt.Sprintf("import %v %q\n", alias, importPath)
}
code += "\nfunc init() {\n"
for _, stmt := range builder.initStatements {
code += "\t" + stmt + "\n"
}
code += "}\n"
// Write to a temp file
tmpDir, err := s.UseTempDir()
if err != nil {
return nil, err
}
f, err := os.CreateTemp(tmpDir, "superpose-*.go")
if err != nil {
return nil, err
}
defer f.Close()
builder.fileName = f.Name()
s.Debugf("Writing the following bridge code to %v:\n%s\n", f.Name(), code)
if _, err := f.Write([]byte(code)); err != nil {
return nil, err
}
return &builder.bridgeFile, nil
}
type bridgeFileBuilder struct {
bridgeFile
imports map[string]string
initStatements []string
// Lazily populated on first file seen
pkgName string
}
// Returns false with no error if we should bail because of parsing issues
func (s *Superpose) buildInitStatements(
ctx context.Context,
builder *bridgeFileBuilder,
goFile string,
) (ok bool, err error) {
// We load the file ahead of time here since we may manip later
b, err := os.ReadFile(goFile)
if err != nil {
return false, err
}
// To save some perf, we're gonna look for the dimension comments anywhere in
// file
var foundDim string
for dim := range s.Config.Transformers {
if bytes.Contains(b, []byte("//"+dim+":")) {
foundDim = dim
break
}
}
if foundDim == "" {
return true, nil
}
// Parse so we can check dim references
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, goFile, b, parser.AllErrors|parser.ParseComments)
// If there's an error parsing, we are going to ignore it because downstream
// will show the error later
if err != nil {
s.Debugf("Ignoring %v, failed parsing: %v", goFile, err)
return false, nil
}
// If the package is _test, fail. Otherwise, check/store package name
if strings.HasSuffix(file.Name.Name, "_test") {
return false, fmt.Errorf("cannot have dimensions in test files, found %v dimension in %v", foundDim, goFile)
} else if builder.pkgName == "" {
builder.pkgName = file.Name.Name
} else if builder.pkgName != file.Name.Name {
// Just ignore this, the actual compiler will report a better error
s.Debugf("Ignoring %v, package %v different than expected %v", goFile, file.Name.Name, builder.pkgName)
return false, nil
}
// Check each top-level var decl for dimension reference and build up
// statements
anyStatements := false
for _, decl := range file.Decls {
// Only var decl
decl, _ := decl.(*ast.GenDecl)
if decl == nil || decl.Tok != token.VAR {
continue
}
for _, spec := range decl.Specs {
// Only vars w/ comments
spec, _ := spec.(*ast.ValueSpec)
if spec == nil || spec.Comment == nil || len(spec.Comment.List) != 1 {
continue
}
// Parse dim:ref
pieces := strings.SplitN(spec.Comment.List[0].Text, ":", 2)
if len(pieces) != 2 {
continue
}
dim, ref := strings.TrimPrefix(pieces[0], "//"), pieces[1]
t := s.Config.Transformers[dim]
// If no transformer or only "<in>", does not apply to us
if t == nil || ref == "<in>" {
continue
}
// The transformer cannot be ignoring this package
applies, err := t.AppliesToPackage(
&TransformContext{Context: ctx, Superpose: s, Dimension: dim},
s.pkgPath,
)
if err != nil {
return false, err
} else if !applies {
return false, fmt.Errorf("dimension %v referenced in package %v, but it is not applied", dim, s.pkgPath)
}
// Validate the var decl
if len(spec.Names) != 1 {
return false, fmt.Errorf("dimension func vars can only have a single identifier")
}
funcType, _ := spec.Type.(*ast.FuncType)
if funcType == nil {
return false, fmt.Errorf("var %v is not typed with a func", spec.Names[0].Name)
} else if len(spec.Values) != 0 {
return false, fmt.Errorf("var %v cannot have default", spec.Names[0].Name)
}
// Find function in same file that is being referenced
var funcDecl *ast.FuncDecl
for _, maybeFuncDecl := range file.Decls {
maybeFuncDecl, _ := maybeFuncDecl.(*ast.FuncDecl)
if maybeFuncDecl != nil && maybeFuncDecl.Name.Name == ref && maybeFuncDecl.Recv == nil {
funcDecl = maybeFuncDecl
break
}
}
if funcDecl == nil {
return false, fmt.Errorf("unable to find func decl %v", ref)
} else if !funcDecl.Name.IsExported() {
return false, fmt.Errorf("referenced dimension bridge function %v is not exported", ref)
}
// Confirm the signatures are identical (param names and everything). Just
// do a string print of the types to confirm.
var expected, actual strings.Builder
if err := printer.Fprint(&expected, fset, funcType); err != nil {
return false, err
} else if err := printer.Fprint(&actual, fset, funcDecl.Type); err != nil {
return false, err
} else if expected.String() != actual.String() {
return false, fmt.Errorf("expected var %v to have type %v, instead had %v",
spec.Names[0].Name, expected.String(), actual.String())
}
// Now confirmed, add init statement
s.Debugf("Setting var %v to function reference of %v in dimension %v", spec.Names[0].Name, ref, dim)
builder.dimPkgRefs.addRef(s.pkgPath, dim)
importAlias := builder.importAlias(s.DimensionPackagePath(s.pkgPath, dim))
builder.initStatements = append(builder.initStatements,
fmt.Sprintf("%v = %v.%v", spec.Names[0].Name, importAlias, ref))
anyStatements = true
}
}
// We expected at least one
if !anyStatements {
return false, fmt.Errorf("no proper dimension references found, though %v referenced", foundDim)
}
return true, nil
}
func (b *bridgeFileBuilder) importAlias(importPath string) string {
alias := b.imports[importPath]
if alias == "" {
alias = fmt.Sprintf("import%v", len(b.imports)+1)
b.imports[importPath] = alias
}
return alias
}