forked from gopherjs/gopherjs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
de142fb
commit 269ba04
Showing
2 changed files
with
321 additions
and
312 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
package build | ||
|
||
import ( | ||
"fmt" | ||
"go/ast" | ||
"go/parser" | ||
"go/token" | ||
"path" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/gopherjs/gopherjs/compiler/astutil" | ||
) | ||
|
||
// overrideInfo is used by parseAndAugment methods to manage | ||
// directives and how the overlay and original are merged. | ||
type overrideInfo struct { | ||
// KeepOriginal indicates that the original code should be kept | ||
// but the identifier will be prefixed by `_gopherjs_original_foo`. | ||
// If false the original code is removed. | ||
keepOriginal bool | ||
|
||
// purgeMethods indicates that this info is for a type and | ||
// if a method has this type as a receiver should also be removed. | ||
// If the method is defined in the overlays and therefore has its | ||
// own overrides, this will be ignored. | ||
purgeMethods bool | ||
|
||
// overrideSignature is the function definition given in the overlays | ||
// that should be used to replace the signature in the originals. | ||
// Only receivers, type parameters, parameters, and results will be used. | ||
overrideSignature *ast.FuncDecl | ||
} | ||
|
||
// pkgOverrideInfo is the collection of overrides still needed for a package. | ||
type pkgOverrideInfo struct { | ||
// overrides is a map of identifier to overrideInfo to override | ||
// individual named structs, interfaces, functions, and methods. | ||
overrides map[string]overrideInfo | ||
|
||
// overlayFiles are the files from the natives that still haven't been | ||
// appended to a file from the package, typically the first file. | ||
overlayFiles []*ast.File | ||
|
||
// jsFiles are the additional JS files that are part of the natives. | ||
jsFiles []JSFile | ||
} | ||
|
||
// Augmentor is an on-the-fly package augmentor. | ||
// | ||
// When a file from a package is being parsed, the Augmentor will augment | ||
// the AST with the changes loaded from the native overrides. | ||
// The augmentor will hold onto the override information for additional files | ||
// that come from the same package. This is designed to be used with | ||
// `x/tools/go/packages.Load` as a middleware in the parse file step via | ||
// `Config.ParseFile`. | ||
// | ||
// The first file from a package will have any additional methods and | ||
// information from the natives injected into the AST. All files from a package | ||
// will be augmented by the overrides. | ||
type Augmentor struct { | ||
// packages is a map of package import path to the package's override. | ||
// This is used to keep track of the overrides for a package and indicate | ||
// that additional files from the natives have already been applied. | ||
packages map[string]*pkgOverrideInfo | ||
} | ||
|
||
func (aug *Augmentor) getPackageOverrides(xctx XContext, pkg *PackageData, fileSet *token.FileSet) *pkgOverrideInfo { | ||
importPath := pkg.ImportPath | ||
if pkgAug, ok := aug.packages[importPath]; ok { | ||
return pkgAug | ||
} | ||
|
||
jsFiles, overlayFiles := parseOverlayFiles(xctx, pkg, fileSet) | ||
|
||
overrides := make(map[string]overrideInfo) | ||
for _, file := range overlayFiles { | ||
augmentOverlayFile(file, overrides) | ||
} | ||
delete(overrides, `init`) | ||
|
||
pkgAug := &pkgOverrideInfo{ | ||
overrides: overrides, | ||
overlayFiles: overlayFiles, | ||
jsFiles: jsFiles, | ||
} | ||
|
||
if aug.packages == nil { | ||
aug.packages = map[string]*pkgOverrideInfo{} | ||
} | ||
aug.packages[importPath] = pkgAug | ||
return pkgAug | ||
} | ||
|
||
func (aug *Augmentor) Augment(xctx XContext, pkg *PackageData, fileSet *token.FileSet, file *ast.File) error { | ||
pkgAug := aug.getPackageOverrides(xctx, pkg, fileSet) | ||
|
||
augmentOriginalImports(pkg.ImportPath, file) | ||
|
||
if len(pkgAug.overrides) > 0 { | ||
augmentOriginalFile(file, pkgAug.overrides) | ||
} | ||
|
||
if len(pkgAug.overlayFiles) > 0 { | ||
// Append the overlay files to the first file of the package. | ||
// This is to ensure that the package is augmented with all the | ||
// additional methods and information from the natives. | ||
err := astutil.ConcatenateFiles(file, pkgAug.overlayFiles...) | ||
if err != nil { | ||
panic(fmt.Errorf("failed to concatenate overlay files onto %q: %w", fileSet.Position(file.Package).Filename, err)) | ||
} | ||
pkgAug.overlayFiles = nil | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// parseOverlayFiles loads and parses overlay files | ||
// to augment the original files with. | ||
func parseOverlayFiles(xctx XContext, pkg *PackageData, fileSet *token.FileSet) ([]JSFile, []*ast.File) { | ||
importPath := pkg.ImportPath | ||
isXTest := strings.HasSuffix(importPath, "_test") | ||
if isXTest { | ||
importPath = importPath[:len(importPath)-5] | ||
} | ||
|
||
nativesContext := overlayCtx(xctx.Env()) | ||
nativesPkg, err := nativesContext.Import(importPath, "", 0) | ||
if err != nil { | ||
return nil, nil | ||
} | ||
|
||
jsFiles := nativesPkg.JSFiles | ||
var files []*ast.File | ||
names := nativesPkg.GoFiles | ||
if pkg.IsTest { | ||
names = append(names, nativesPkg.TestGoFiles...) | ||
} | ||
if isXTest { | ||
names = nativesPkg.XTestGoFiles | ||
} | ||
|
||
for _, name := range names { | ||
fullPath := path.Join(nativesPkg.Dir, name) | ||
r, err := nativesContext.bctx.OpenFile(fullPath) | ||
if err != nil { | ||
panic(err) | ||
} | ||
// Files should be uniquely named and in the original package directory in order to be | ||
// ordered correctly | ||
newPath := path.Join(pkg.Dir, "gopherjs__"+name) | ||
file, err := parser.ParseFile(fileSet, newPath, r, parser.ParseComments) | ||
if err != nil { | ||
panic(err) | ||
} | ||
r.Close() | ||
|
||
files = append(files, file) | ||
} | ||
return jsFiles, files | ||
} | ||
|
||
// augmentOverlayFile is the part of parseAndAugment that processes | ||
// an overlay file AST to collect information such as compiler directives | ||
// and perform any initial augmentation needed to the overlay. | ||
func augmentOverlayFile(file *ast.File, overrides map[string]overrideInfo) { | ||
anyChange := false | ||
for i, decl := range file.Decls { | ||
purgeDecl := astutil.Purge(decl) | ||
switch d := decl.(type) { | ||
case *ast.FuncDecl: | ||
k := astutil.FuncKey(d) | ||
oi := overrideInfo{ | ||
keepOriginal: astutil.KeepOriginal(d), | ||
} | ||
if astutil.OverrideSignature(d) { | ||
oi.overrideSignature = d | ||
purgeDecl = true | ||
} | ||
overrides[k] = oi | ||
case *ast.GenDecl: | ||
for j, spec := range d.Specs { | ||
purgeSpec := purgeDecl || astutil.Purge(spec) | ||
switch s := spec.(type) { | ||
case *ast.TypeSpec: | ||
overrides[s.Name.Name] = overrideInfo{ | ||
purgeMethods: purgeSpec, | ||
} | ||
case *ast.ValueSpec: | ||
for _, name := range s.Names { | ||
overrides[name.Name] = overrideInfo{} | ||
} | ||
} | ||
if purgeSpec { | ||
anyChange = true | ||
d.Specs[j] = nil | ||
} | ||
} | ||
} | ||
if purgeDecl { | ||
anyChange = true | ||
file.Decls[i] = nil | ||
} | ||
} | ||
if anyChange { | ||
astutil.FinalizeRemovals(file) | ||
astutil.PruneImports(file) | ||
} | ||
} | ||
|
||
// augmentOriginalImports is the part of parseAndAugment that processes | ||
// an original file AST to modify the imports for that file. | ||
func augmentOriginalImports(importPath string, file *ast.File) { | ||
switch importPath { | ||
case "crypto/rand", "encoding/gob", "encoding/json", "expvar", "go/token", "log", "math/big", "math/rand", "regexp", "time": | ||
for _, spec := range file.Imports { | ||
path, _ := strconv.Unquote(spec.Path.Value) | ||
if path == "sync" { | ||
if spec.Name == nil { | ||
spec.Name = ast.NewIdent("sync") | ||
} | ||
spec.Path.Value = `"github.com/gopherjs/gopherjs/nosync"` | ||
} | ||
} | ||
} | ||
} | ||
|
||
// augmentOriginalFile is the part of parseAndAugment that processes an | ||
// original file AST to augment the source code using the overrides from | ||
// the overlay files. | ||
func augmentOriginalFile(file *ast.File, overrides map[string]overrideInfo) { | ||
anyChange := false | ||
for i, decl := range file.Decls { | ||
switch d := decl.(type) { | ||
case *ast.FuncDecl: | ||
if info, ok := overrides[astutil.FuncKey(d)]; ok { | ||
anyChange = true | ||
removeFunc := true | ||
if info.keepOriginal { | ||
// Allow overridden function calls | ||
// The standard library implementation of foo() becomes _gopherjs_original_foo() | ||
d.Name.Name = "_gopherjs_original_" + d.Name.Name | ||
removeFunc = false | ||
} | ||
if overSig := info.overrideSignature; overSig != nil { | ||
d.Recv = overSig.Recv | ||
d.Type.TypeParams = overSig.Type.TypeParams | ||
d.Type.Params = overSig.Type.Params | ||
d.Type.Results = overSig.Type.Results | ||
removeFunc = false | ||
} | ||
if removeFunc { | ||
file.Decls[i] = nil | ||
} | ||
} else if recvKey := astutil.FuncReceiverKey(d); len(recvKey) > 0 { | ||
// check if the receiver has been purged, if so, remove the method too. | ||
if info, ok := overrides[recvKey]; ok && info.purgeMethods { | ||
anyChange = true | ||
file.Decls[i] = nil | ||
} | ||
} | ||
case *ast.GenDecl: | ||
for j, spec := range d.Specs { | ||
switch s := spec.(type) { | ||
case *ast.TypeSpec: | ||
if _, ok := overrides[s.Name.Name]; ok { | ||
anyChange = true | ||
d.Specs[j] = nil | ||
} | ||
case *ast.ValueSpec: | ||
if len(s.Names) == len(s.Values) { | ||
// multi-value context | ||
// e.g. var a, b = 2, foo[int]() | ||
// A removal will also remove the value which may be from a | ||
// function call. This allows us to remove unwanted statements. | ||
// However, if that call has a side effect which still needs | ||
// to be run, add the call into the overlay. | ||
for k, name := range s.Names { | ||
if _, ok := overrides[name.Name]; ok { | ||
anyChange = true | ||
s.Names[k] = nil | ||
s.Values[k] = nil | ||
} | ||
} | ||
} else { | ||
// single-value context | ||
// e.g. var a, b = foo[int]() | ||
// If a removal from the overlays makes all returned values unused, | ||
// then remove the function call as well. This allows us to stop | ||
// unwanted calls if needed. If that call has a side effect which | ||
// still needs to be run, add the call into the overlay. | ||
nameRemoved := false | ||
for _, name := range s.Names { | ||
if _, ok := overrides[name.Name]; ok { | ||
nameRemoved = true | ||
name.Name = `_` | ||
} | ||
} | ||
if nameRemoved { | ||
removeSpec := true | ||
for _, name := range s.Names { | ||
if name.Name != `_` { | ||
removeSpec = false | ||
break | ||
} | ||
} | ||
if removeSpec { | ||
anyChange = true | ||
d.Specs[j] = nil | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
if anyChange { | ||
astutil.FinalizeRemovals(file) | ||
astutil.PruneImports(file) | ||
} | ||
} |
Oops, something went wrong.