Skip to content

Commit 437309a

Browse files
Adding Augmentor
1 parent 289ebba commit 437309a

File tree

5 files changed

+545
-398
lines changed

5 files changed

+545
-398
lines changed

build/augmentor.go

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package build
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/parser"
7+
"go/token"
8+
"path"
9+
"strconv"
10+
"strings"
11+
12+
"github.com/gopherjs/gopherjs/compiler/astutil"
13+
)
14+
15+
// overrideInfo is used by parseAndAugment methods to manage
16+
// directives and how the overlay and original are merged.
17+
type overrideInfo struct {
18+
// KeepOriginal indicates that the original code should be kept
19+
// but the identifier will be prefixed by `_gopherjs_original_foo`.
20+
// If false the original code is removed.
21+
keepOriginal bool
22+
23+
// purgeMethods indicates that this info is for a type and
24+
// if a method has this type as a receiver should also be removed.
25+
// If the method is defined in the overlays and therefore has its
26+
// own overrides, this will be ignored.
27+
purgeMethods bool
28+
29+
// overrideSignature is the function definition given in the overlays
30+
// that should be used to replace the signature in the originals.
31+
// Only receivers, type parameters, parameters, and results will be used.
32+
overrideSignature *ast.FuncDecl
33+
}
34+
35+
// pkgOverrideInfo is the collection of overrides still needed for a package.
36+
type pkgOverrideInfo struct {
37+
// overrides is a map of identifier to overrideInfo to override
38+
// individual named structs, interfaces, functions, and methods.
39+
overrides map[string]overrideInfo
40+
41+
// overlayFiles are the files from the natives that still haven't been
42+
// appended to a file from the package, typically the first file.
43+
overlayFiles []*ast.File
44+
45+
// jsFiles are the additional JS files that are part of the natives.
46+
jsFiles []JSFile
47+
}
48+
49+
// Augmentor is an on-the-fly package augmentor.
50+
//
51+
// When a file from a package is being parsed, the Augmentor will augment
52+
// the AST with the changes loaded from the native overrides.
53+
// The augmentor will hold onto the override information for additional files
54+
// that come from the same package. This is designed to be used with
55+
// `x/tools/go/packages.Load` as a middleware in the parse file step via
56+
// `Config.ParseFile`.
57+
//
58+
// The first file from a package will have any additional methods and
59+
// information from the natives injected into the AST. All files from a package
60+
// will be augmented by the overrides.
61+
type Augmentor struct {
62+
// packages is a map of package import path to the package's override.
63+
// This is used to keep track of the overrides for a package and indicate
64+
// that additional files from the natives have already been applied.
65+
packages map[string]*pkgOverrideInfo
66+
}
67+
68+
func (aug *Augmentor) getPackageOverrides(xctx XContext, pkg *PackageData, fileSet *token.FileSet) *pkgOverrideInfo {
69+
importPath := pkg.ImportPath
70+
if pkgAug, ok := aug.packages[importPath]; ok {
71+
return pkgAug
72+
}
73+
74+
jsFiles, overlayFiles := parseOverlayFiles(xctx, pkg, fileSet)
75+
76+
overrides := make(map[string]overrideInfo)
77+
for _, file := range overlayFiles {
78+
augmentOverlayFile(file, overrides)
79+
}
80+
delete(overrides, `init`)
81+
82+
pkgAug := &pkgOverrideInfo{
83+
overrides: overrides,
84+
overlayFiles: overlayFiles,
85+
jsFiles: jsFiles,
86+
}
87+
88+
if aug.packages == nil {
89+
aug.packages = map[string]*pkgOverrideInfo{}
90+
}
91+
aug.packages[importPath] = pkgAug
92+
return pkgAug
93+
}
94+
95+
func (aug *Augmentor) Augment(xctx XContext, pkg *PackageData, fileSet *token.FileSet, file *ast.File) error {
96+
pkgAug := aug.getPackageOverrides(xctx, pkg, fileSet)
97+
98+
augmentOriginalImports(pkg.ImportPath, file)
99+
100+
if len(pkgAug.overrides) > 0 {
101+
augmentOriginalFile(file, pkgAug.overrides)
102+
}
103+
104+
if len(pkgAug.overlayFiles) > 0 {
105+
// Append the overlay files to the first file of the package.
106+
// This is to ensure that the package is augmented with all the
107+
// additional methods and information from the natives.
108+
err := astutil.ConcatenateFiles(file, pkgAug.overlayFiles...)
109+
if err != nil {
110+
panic(fmt.Errorf("failed to concatenate overlay files onto %q: %w", fileSet.Position(file.Package).Filename, err))
111+
}
112+
pkgAug.overlayFiles = nil
113+
}
114+
115+
return nil
116+
}
117+
118+
// parseOverlayFiles loads and parses overlay files
119+
// to augment the original files with.
120+
func parseOverlayFiles(xctx XContext, pkg *PackageData, fileSet *token.FileSet) ([]JSFile, []*ast.File) {
121+
importPath := pkg.ImportPath
122+
isXTest := strings.HasSuffix(importPath, "_test")
123+
if isXTest {
124+
importPath = importPath[:len(importPath)-5]
125+
}
126+
127+
nativesContext := overlayCtx(xctx.Env())
128+
nativesPkg, err := nativesContext.Import(importPath, "", 0)
129+
if err != nil {
130+
return nil, nil
131+
}
132+
133+
jsFiles := nativesPkg.JSFiles
134+
var files []*ast.File
135+
names := nativesPkg.GoFiles
136+
if pkg.IsTest {
137+
names = append(names, nativesPkg.TestGoFiles...)
138+
}
139+
if isXTest {
140+
names = nativesPkg.XTestGoFiles
141+
}
142+
143+
for _, name := range names {
144+
fullPath := path.Join(nativesPkg.Dir, name)
145+
r, err := nativesContext.bctx.OpenFile(fullPath)
146+
if err != nil {
147+
panic(err)
148+
}
149+
// Files should be uniquely named and in the original package directory in order to be
150+
// ordered correctly
151+
newPath := path.Join(pkg.Dir, "gopherjs__"+name)
152+
file, err := parser.ParseFile(fileSet, newPath, r, parser.ParseComments)
153+
if err != nil {
154+
panic(err)
155+
}
156+
r.Close()
157+
158+
files = append(files, file)
159+
}
160+
return jsFiles, files
161+
}
162+
163+
// augmentOverlayFile is the part of parseAndAugment that processes
164+
// an overlay file AST to collect information such as compiler directives
165+
// and perform any initial augmentation needed to the overlay.
166+
func augmentOverlayFile(file *ast.File, overrides map[string]overrideInfo) {
167+
anyChange := false
168+
for i, decl := range file.Decls {
169+
purgeDecl := astutil.Purge(decl)
170+
switch d := decl.(type) {
171+
case *ast.FuncDecl:
172+
k := astutil.FuncKey(d)
173+
oi := overrideInfo{
174+
keepOriginal: astutil.KeepOriginal(d),
175+
}
176+
if astutil.OverrideSignature(d) {
177+
oi.overrideSignature = d
178+
purgeDecl = true
179+
}
180+
overrides[k] = oi
181+
case *ast.GenDecl:
182+
for j, spec := range d.Specs {
183+
purgeSpec := purgeDecl || astutil.Purge(spec)
184+
switch s := spec.(type) {
185+
case *ast.TypeSpec:
186+
overrides[s.Name.Name] = overrideInfo{
187+
purgeMethods: purgeSpec,
188+
}
189+
case *ast.ValueSpec:
190+
for _, name := range s.Names {
191+
overrides[name.Name] = overrideInfo{}
192+
}
193+
}
194+
if purgeSpec {
195+
anyChange = true
196+
d.Specs[j] = nil
197+
}
198+
}
199+
}
200+
if purgeDecl {
201+
anyChange = true
202+
file.Decls[i] = nil
203+
}
204+
}
205+
if anyChange {
206+
astutil.FinalizeRemovals(file)
207+
astutil.PruneImports(file)
208+
}
209+
}
210+
211+
// augmentOriginalImports is the part of parseAndAugment that processes
212+
// an original file AST to modify the imports for that file.
213+
func augmentOriginalImports(importPath string, file *ast.File) {
214+
switch importPath {
215+
case "crypto/rand", "encoding/gob", "encoding/json", "expvar", "go/token", "log", "math/big", "math/rand", "regexp", "time":
216+
for _, spec := range file.Imports {
217+
path, _ := strconv.Unquote(spec.Path.Value)
218+
if path == "sync" {
219+
if spec.Name == nil {
220+
spec.Name = ast.NewIdent("sync")
221+
}
222+
spec.Path.Value = `"github.com/gopherjs/gopherjs/nosync"`
223+
}
224+
}
225+
}
226+
}
227+
228+
// augmentOriginalFile is the part of parseAndAugment that processes an
229+
// original file AST to augment the source code using the overrides from
230+
// the overlay files.
231+
func augmentOriginalFile(file *ast.File, overrides map[string]overrideInfo) {
232+
anyChange := false
233+
for i, decl := range file.Decls {
234+
switch d := decl.(type) {
235+
case *ast.FuncDecl:
236+
if info, ok := overrides[astutil.FuncKey(d)]; ok {
237+
anyChange = true
238+
removeFunc := true
239+
if info.keepOriginal {
240+
// Allow overridden function calls
241+
// The standard library implementation of foo() becomes _gopherjs_original_foo()
242+
d.Name.Name = "_gopherjs_original_" + d.Name.Name
243+
removeFunc = false
244+
}
245+
if overSig := info.overrideSignature; overSig != nil {
246+
d.Recv = overSig.Recv
247+
d.Type.TypeParams = overSig.Type.TypeParams
248+
d.Type.Params = overSig.Type.Params
249+
d.Type.Results = overSig.Type.Results
250+
removeFunc = false
251+
}
252+
if removeFunc {
253+
file.Decls[i] = nil
254+
}
255+
} else if recvKey := astutil.FuncReceiverKey(d); len(recvKey) > 0 {
256+
// check if the receiver has been purged, if so, remove the method too.
257+
if info, ok := overrides[recvKey]; ok && info.purgeMethods {
258+
anyChange = true
259+
file.Decls[i] = nil
260+
}
261+
}
262+
case *ast.GenDecl:
263+
for j, spec := range d.Specs {
264+
switch s := spec.(type) {
265+
case *ast.TypeSpec:
266+
if _, ok := overrides[s.Name.Name]; ok {
267+
anyChange = true
268+
d.Specs[j] = nil
269+
}
270+
case *ast.ValueSpec:
271+
if len(s.Names) == len(s.Values) {
272+
// multi-value context
273+
// e.g. var a, b = 2, foo[int]()
274+
// A removal will also remove the value which may be from a
275+
// function call. This allows us to remove unwanted statements.
276+
// However, if that call has a side effect which still needs
277+
// to be run, add the call into the overlay.
278+
for k, name := range s.Names {
279+
if _, ok := overrides[name.Name]; ok {
280+
anyChange = true
281+
s.Names[k] = nil
282+
s.Values[k] = nil
283+
}
284+
}
285+
} else {
286+
// single-value context
287+
// e.g. var a, b = foo[int]()
288+
// If a removal from the overlays makes all returned values unused,
289+
// then remove the function call as well. This allows us to stop
290+
// unwanted calls if needed. If that call has a side effect which
291+
// still needs to be run, add the call into the overlay.
292+
nameRemoved := false
293+
for _, name := range s.Names {
294+
if _, ok := overrides[name.Name]; ok {
295+
nameRemoved = true
296+
name.Name = `_`
297+
}
298+
}
299+
if nameRemoved {
300+
removeSpec := true
301+
for _, name := range s.Names {
302+
if name.Name != `_` {
303+
removeSpec = false
304+
break
305+
}
306+
}
307+
if removeSpec {
308+
anyChange = true
309+
d.Specs[j] = nil
310+
}
311+
}
312+
}
313+
}
314+
}
315+
}
316+
}
317+
if anyChange {
318+
astutil.FinalizeRemovals(file)
319+
astutil.PruneImports(file)
320+
}
321+
}

0 commit comments

Comments
 (0)