Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/lint_allowed_geth_imports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ set -o pipefail
# 1. Recursively search through all go files for any lines that include a direct import from go-ethereum
# 2. Sort the unique results
# #. Print out the difference between the search results and the list of specified allowed package imports from geth.
extra_imports=$(grep -r --include='*.go' --exclude-dir='simulator' '"github.com/ethereum/go-ethereum/.*"' -o -h | sort -u | comm -23 - ./scripts/geth-allowed-packages.txt)
extra_imports=$(grep -Pr --include='*.go' --exclude-dir='simulator' '^\s+"github.com/ethereum/go-ethereum/.*"' -o -h | tr -d '[:blank:]' | sort -u | comm -23 - ./scripts/geth-allowed-packages.txt)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a false positive on const geth = "github.com/ethereum/go-ethereum/" so I updated the check to only match with (possible) whitespace but no other prefix.

if [ -n "${extra_imports}" ]; then
echo "new go-ethereum imports should be added to ./scripts/geth-allowed-packages.txt to prevent accidental imports:"
echo "${extra_imports}"
Expand Down
3 changes: 3 additions & 0 deletions x/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Experimental

Code in this directory is experimental and MUST NOT be relied on to be stable.
5 changes: 5 additions & 0 deletions x/gethclone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# `gethclone`

This is an experimental module for tracking upstream `go-ethereum` changes.
The approach of `git merge`ing the upstream branch into `subnet-evm` is brittle as it relies on purely syntactic patching.
`gethclone` is intended to follow a rebase-like pattern, applying a set of semantic patches (e.g. AST modification, [Uber's `gopatch`](https://pkg.go.dev/github.com/uber-go/gopatch), etc.) that (a) should be more robust to refactoring; and (b) act as explicit documentation of the diffs.
91 changes: 91 additions & 0 deletions x/gethclone/astpatch/astpatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Package astpatch provides functionality for traversing and modifying Go
// syntax trees. It extends the astutil package with reusable "patches".
package astpatch

import (
"go/ast"
"reflect"

"golang.org/x/tools/go/ast/astutil"
)

type (
// A Patch (optionally) modifies an AST node; it is equivalent to an
// `astutil.ApplyFunc` except that it returns an error instead of a boolean.
// A non-nil error is equivalent to returning false and will also abort all
// further calls to other patches.
Patch func(*astutil.Cursor) error
// A PatchRegistry maps [Go package path] -> [ast.Node concrete types] ->
// [all `Patch` functions that must be applied to said node types in said
// package].
//
// The special `pkgPath` value "*" will match all package paths.
PatchRegistry map[string]map[reflect.Type][]Patch
)

// Add is a convenience wrapper for registering a new `Patch` in the registry.
// The `zeroNode` can be any type (including nil pointers) that implements
// `ast.Node`.
//
// The special `pkgPath` value "*" will match all package paths. While there is
// no specific requirement for `pkgPath` other than it matching the equivalent
// argument passed to `Apply()`, it is typically sourced from
// `golang.org/x/tools/go/packages.Package.PkgPath`.
func (r PatchRegistry) Add(pkgPath string, zeroNode ast.Node, fn Patch) {
pkg, ok := r[pkgPath]
if !ok {
pkg = make(map[reflect.Type][]Patch)
r[pkgPath] = pkg
}

t := nodeType(zeroNode)
pkg[t] = append(pkg[t], fn)
}

// Apply calls `astutil.Apply()` on `node`, calling the appropriate `Patch`
// functions as the syntax tree is traversed. Patches are applied as the `pre`
// argument to `astutil.Apply()`.
//
// Global `pkgPath` matches (i.e. to those registered with "*") will be applied
// before package-specific matches.
//
// If any `Patch` returns an error then no further patches will be called, and
// the error will be returned by `Apply()`.
func (r PatchRegistry) Apply(pkgPath string, node ast.Node) error {
var err error
astutil.Apply(node, func(c *astutil.Cursor) bool {
if err != nil {
return false
}
if err = r.applyToCursor("*", c); err != nil {
return false
}
err = r.applyToCursor(pkgPath, c)
return err == nil
}, nil)
return err
}

func (r PatchRegistry) applyToCursor(pkgPath string, c *astutil.Cursor) error {
if c.Node() == nil {
return nil
}

pkg, ok := r[pkgPath]
if !ok {
return nil
}
for _, fn := range pkg[nodeType(c.Node())] {
if err := fn(c); err != nil {
return err
}
}
return nil
}

// nodeType returns the `reflect.Type` of the _concrete_ type implementing
// `ast.Node`. Simpy calling `reflect.TypeOf(n)` would be incorrect as it would
// reflect the interface (and not match any nodes).
func nodeType(n ast.Node) reflect.Type {
return reflect.ValueOf(n).Type()
}
121 changes: 121 additions & 0 deletions x/gethclone/astpatch/astpatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package astpatch

import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/ast/astutil"
)

type patchSpy struct {
gotFuncs, gotStructs []string
}

const errorIfFuncName = "ErrorFuncName"

var errFuncName = fmt.Errorf("encountered sentinel function %q", errorIfFuncName)

func (s *patchSpy) funcRecorder(c *astutil.Cursor) error {
name := c.Node().(*ast.FuncDecl).Name.String()
if name == errorIfFuncName {
return errFuncName
}
s.gotFuncs = append(s.gotFuncs, name)
return nil
}

func (s *patchSpy) structRecorder(c *astutil.Cursor) error {
switch p := c.Parent().(type) {
case *ast.TypeSpec: // it's a `type x struct` not, for example, a `map[T]struct{}`
s.gotStructs = append(s.gotStructs, p.Name.String())
}
return nil
}

func TestPatchRegistry(t *testing.T) {
tests := []struct {
name string
src string
wantErr error
wantFuncs, wantStructs []string
}{
{
name: "happy path",
src: `package thepackage

func FnA(){}

func FnB(){}

type StructA struct{}

type StructB struct{}
`,
wantFuncs: []string{"FnA", "FnB"},
wantStructs: []string{"StructA", "StructB"},
},
{
name: "error propagation",
src: `package thepackage

func HappyFn() {}

func ` + errorIfFuncName + `() {}
`,
wantErr: errFuncName,
wantFuncs: []string{"HappyFn"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var spy patchSpy
reg := make(PatchRegistry)

reg.Add("*", &ast.FuncDecl{}, spy.funcRecorder)
const pkgPath = `github.com/the/repo/thepackage`
reg.Add(pkgPath, &ast.StructType{}, spy.structRecorder)

reg.Add("unknown/package/path", &ast.FuncDecl{}, func(c *astutil.Cursor) error {
t.Errorf("unexpected call to %T with different package path", (Patch)(nil))
return nil
})

file := parseGoFile(t, token.NewFileSet(), tt.src)
bestEffortLogAST(t, file)

// None of the `require.Equal*()` variants provide a check for exact
// match (i.e. equivalent to ==) of the identical error being
// propagated.
if gotErr := reg.Apply(pkgPath, file); gotErr != tt.wantErr {
t.Fatalf("%T.Apply(...) got err %v; want %v", reg, gotErr, tt.wantErr)
}
assert.Empty(t, cmp.Diff(tt.wantFuncs, spy.gotFuncs), "encountered function declarations (-want +got)")
assert.Empty(t, cmp.Diff(tt.wantStructs, spy.gotStructs), "encountered struct-type declarations (-want +got)")
})
}
}

func parseGoFile(tb testing.TB, fset *token.FileSet, src string) *ast.File {
tb.Helper()
f, err := parser.ParseFile(fset, "", src, parser.SkipObjectResolution)
require.NoError(tb, err, "Parsing Go source as file: parser.ParseFile([see logged source])")
return f
}

func bestEffortLogAST(tb testing.TB, x any) {
tb.Helper()

var buf bytes.Buffer
if err := ast.Fprint(&buf, nil, x, nil); err != nil {
return
}
tb.Logf("AST of parsed source:\n\n%s", buf.String())
}
14 changes: 14 additions & 0 deletions x/gethclone/copyright.go.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// (c) 2019-2024, Ava Labs, Inc.
//
// This file is a derived work, based on the go-ethereum library whose original
// notices appear below.
//
// It is distributed under a license compatible with the licensing terms of the
// original code from which it is derived.
//
// Much love to the original authors for their work.
// **********
//
// Code generated by gethclone - DO NOT EDIT.
//
// **********
Loading