Skip to content

Commit 889937f

Browse files
Adding Augmentor
1 parent 289ebba commit 889937f

File tree

6 files changed

+1157
-406
lines changed

6 files changed

+1157
-406
lines changed

build/augmentor.go

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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) Augment(xctx XContext, pkg *PackageData, fileSet *token.FileSet, file *ast.File) error {
69+
pkgAug := aug.getPackageOverrides(xctx, pkg, fileSet)
70+
71+
augmentOriginalImports(pkg.ImportPath, file)
72+
73+
if len(pkgAug.overrides) > 0 {
74+
augmentOriginalFile(file, pkgAug.overrides)
75+
}
76+
77+
if len(pkgAug.overlayFiles) > 0 {
78+
// Append the overlay files to the first file of the package.
79+
// This is to ensure that the package is augmented with all the
80+
// additional methods and information from the natives.
81+
err := astutil.ConcatenateFiles(file, pkgAug.overlayFiles...)
82+
if err != nil {
83+
panic(fmt.Errorf("failed to concatenate overlay files onto %q: %w", fileSet.Position(file.Package).Filename, err))
84+
}
85+
pkgAug.overlayFiles = nil
86+
}
87+
88+
return nil
89+
}
90+
91+
// getPackageOverrides looks up an already loaded package override
92+
// or loads the package's natives, parses the overlay files, and
93+
// stores the overrides for the package in the augmentor for next time.
94+
func (aug *Augmentor) getPackageOverrides(xctx XContext, pkg *PackageData, fileSet *token.FileSet) *pkgOverrideInfo {
95+
importPath := pkg.ImportPath
96+
if pkgAug, ok := aug.packages[importPath]; ok {
97+
return pkgAug
98+
}
99+
100+
jsFiles, overlayFiles := parseOverlayFiles(xctx, pkg, fileSet)
101+
102+
overrides := make(map[string]overrideInfo)
103+
for _, file := range overlayFiles {
104+
augmentOverlayFile(file, overrides)
105+
}
106+
delete(overrides, `init`)
107+
108+
pkgAug := &pkgOverrideInfo{
109+
overrides: overrides,
110+
overlayFiles: overlayFiles,
111+
jsFiles: jsFiles,
112+
}
113+
114+
if aug.packages == nil {
115+
aug.packages = map[string]*pkgOverrideInfo{}
116+
}
117+
aug.packages[importPath] = pkgAug
118+
return pkgAug
119+
}
120+
121+
// parseOverlayFiles loads and parses overlay files
122+
// to augment the original files with.
123+
func parseOverlayFiles(xctx XContext, pkg *PackageData, fileSet *token.FileSet) ([]JSFile, []*ast.File) {
124+
importPath := pkg.ImportPath
125+
isXTest := strings.HasSuffix(importPath, "_test")
126+
if isXTest {
127+
importPath = importPath[:len(importPath)-5]
128+
}
129+
130+
nativesContext := overlayCtx(xctx.Env())
131+
nativesPkg, err := nativesContext.Import(importPath, "", 0)
132+
if err != nil {
133+
return nil, nil
134+
}
135+
136+
jsFiles := nativesPkg.JSFiles
137+
var files []*ast.File
138+
names := nativesPkg.GoFiles
139+
if pkg.IsTest {
140+
names = append(names, nativesPkg.TestGoFiles...)
141+
}
142+
if isXTest {
143+
names = nativesPkg.XTestGoFiles
144+
}
145+
146+
for _, name := range names {
147+
fullPath := path.Join(nativesPkg.Dir, name)
148+
r, err := nativesContext.bctx.OpenFile(fullPath)
149+
if err != nil {
150+
panic(err)
151+
}
152+
// Files should be uniquely named and in the original package directory in order to be
153+
// ordered correctly
154+
newPath := path.Join(pkg.Dir, "gopherjs__"+name)
155+
file, err := parser.ParseFile(fileSet, newPath, r, parser.ParseComments)
156+
if err != nil {
157+
panic(err)
158+
}
159+
r.Close()
160+
161+
files = append(files, file)
162+
}
163+
return jsFiles, files
164+
}
165+
166+
// augmentOverlayFile is the part of parseAndAugment that processes
167+
// an overlay file AST to collect information such as compiler directives
168+
// and perform any initial augmentation needed to the overlay.
169+
func augmentOverlayFile(file *ast.File, overrides map[string]overrideInfo) {
170+
anyChange := false
171+
for i, decl := range file.Decls {
172+
purgeDecl := astutil.Purge(decl)
173+
switch d := decl.(type) {
174+
case *ast.FuncDecl:
175+
k := astutil.FuncKey(d)
176+
oi := overrideInfo{
177+
keepOriginal: astutil.KeepOriginal(d),
178+
}
179+
if astutil.OverrideSignature(d) {
180+
oi.overrideSignature = d
181+
purgeDecl = true
182+
}
183+
overrides[k] = oi
184+
case *ast.GenDecl:
185+
for j, spec := range d.Specs {
186+
purgeSpec := purgeDecl || astutil.Purge(spec)
187+
switch s := spec.(type) {
188+
case *ast.TypeSpec:
189+
overrides[s.Name.Name] = overrideInfo{
190+
purgeMethods: purgeSpec,
191+
}
192+
case *ast.ValueSpec:
193+
for _, name := range s.Names {
194+
overrides[name.Name] = overrideInfo{}
195+
}
196+
}
197+
if purgeSpec {
198+
anyChange = true
199+
d.Specs[j] = nil
200+
}
201+
}
202+
}
203+
if purgeDecl {
204+
anyChange = true
205+
file.Decls[i] = nil
206+
}
207+
}
208+
if anyChange {
209+
astutil.FinalizeRemovals(file)
210+
astutil.PruneImports(file)
211+
}
212+
}
213+
214+
// augmentOriginalImports is the part of parseAndAugment that processes
215+
// an original file AST to modify the imports for that file.
216+
func augmentOriginalImports(importPath string, file *ast.File) {
217+
switch importPath {
218+
case "crypto/rand", "encoding/gob", "encoding/json", "expvar", "go/token", "log", "math/big", "math/rand", "regexp", "time":
219+
for _, spec := range file.Imports {
220+
path, _ := strconv.Unquote(spec.Path.Value)
221+
if path == "sync" {
222+
if spec.Name == nil {
223+
spec.Name = ast.NewIdent("sync")
224+
}
225+
spec.Path.Value = `"github.com/gopherjs/gopherjs/nosync"`
226+
}
227+
}
228+
}
229+
}
230+
231+
// augmentOriginalFile is the part of parseAndAugment that processes an
232+
// original file AST to augment the source code using the overrides from
233+
// the overlay files.
234+
func augmentOriginalFile(file *ast.File, overrides map[string]overrideInfo) {
235+
anyChange := false
236+
for i, decl := range file.Decls {
237+
switch d := decl.(type) {
238+
case *ast.FuncDecl:
239+
if info, ok := overrides[astutil.FuncKey(d)]; ok {
240+
anyChange = true
241+
removeFunc := true
242+
if info.keepOriginal {
243+
// Allow overridden function calls
244+
// The standard library implementation of foo() becomes _gopherjs_original_foo()
245+
d.Name.Name = "_gopherjs_original_" + d.Name.Name
246+
removeFunc = false
247+
}
248+
if overSig := info.overrideSignature; overSig != nil {
249+
d.Recv = overSig.Recv
250+
d.Type.TypeParams = overSig.Type.TypeParams
251+
d.Type.Params = overSig.Type.Params
252+
d.Type.Results = overSig.Type.Results
253+
removeFunc = false
254+
}
255+
if removeFunc {
256+
file.Decls[i] = nil
257+
}
258+
} else if recvKey := astutil.FuncReceiverKey(d); len(recvKey) > 0 {
259+
// check if the receiver has been purged, if so, remove the method too.
260+
if info, ok := overrides[recvKey]; ok && info.purgeMethods {
261+
anyChange = true
262+
file.Decls[i] = nil
263+
}
264+
}
265+
case *ast.GenDecl:
266+
for j, spec := range d.Specs {
267+
switch s := spec.(type) {
268+
case *ast.TypeSpec:
269+
if _, ok := overrides[s.Name.Name]; ok {
270+
anyChange = true
271+
d.Specs[j] = nil
272+
}
273+
case *ast.ValueSpec:
274+
if len(s.Names) == len(s.Values) {
275+
// multi-value context
276+
// e.g. var a, b = 2, foo[int]()
277+
// A removal will also remove the value which may be from a
278+
// function call. This allows us to remove unwanted statements.
279+
// However, if that call has a side effect which still needs
280+
// to be run, add the call into the overlay.
281+
for k, name := range s.Names {
282+
if _, ok := overrides[name.Name]; ok {
283+
anyChange = true
284+
s.Names[k] = nil
285+
s.Values[k] = nil
286+
}
287+
}
288+
} else {
289+
// single-value context
290+
// e.g. var a, b = foo[int]()
291+
// If a removal from the overlays makes all returned values unused,
292+
// then remove the function call as well. This allows us to stop
293+
// unwanted calls if needed. If that call has a side effect which
294+
// still needs to be run, add the call into the overlay.
295+
nameRemoved := false
296+
for _, name := range s.Names {
297+
if _, ok := overrides[name.Name]; ok {
298+
nameRemoved = true
299+
name.Name = `_`
300+
}
301+
}
302+
if nameRemoved {
303+
removeSpec := true
304+
for _, name := range s.Names {
305+
if name.Name != `_` {
306+
removeSpec = false
307+
break
308+
}
309+
}
310+
if removeSpec {
311+
anyChange = true
312+
d.Specs[j] = nil
313+
}
314+
}
315+
}
316+
}
317+
}
318+
}
319+
}
320+
if anyChange {
321+
astutil.FinalizeRemovals(file)
322+
astutil.PruneImports(file)
323+
}
324+
}

0 commit comments

Comments
 (0)