Skip to content

Commit 6fa9024

Browse files
Adding Augmentor
1 parent 289ebba commit 6fa9024

File tree

7 files changed

+1225
-420
lines changed

7 files changed

+1225
-420
lines changed

build/augmentor.go

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

0 commit comments

Comments
 (0)