Skip to content

Conversation

@JonathanOppenheimer
Copy link
Member

@JonathanOppenheimer JonathanOppenheimer commented Nov 21, 2025

Why this should be merged

This PR accomplishes three things:

  1. Prevent files inside of the vms/evm directory from importing packages within the graft/ directory.
    • This is important because with the removal of coreth, there is not longer a natural boundary between the uplifted and non-uplifted code. We need to enforce such a boundary.
    • This boundary is important, especially as other people individuals start work on properly uplfiting code post monorepo. Reviewers and developers may not be familiar with why we want this distinction to exist and programmatically enforcing this boundary will prevent mistakes and leakage of legacy EVM code into AvalancheGo.
  2. Prevent libevm/pseudo from being imported anywhere in AvalancheGo

How this works

The boundaries are as follows:

  • graft/coreth can be imported everywhere in AvalancheGo except vms/evm.
    • However, vms/evm/emulate is an exception to this rule
  • graft/subnet-evm can not be imported anywhere in AvalancheGo
    • However, vms/evm/emulate is an exception to this rule

How this was tested

Existing CI. I also added a sample "bad" commit (404acd5) to show how CI would behave.
Screenshot 2025-11-21 at 6 34 17 PM

edit: this is no longer a good example and is out of date but I kept it to show the formatting -- pretend the file is in vms/evm/sample_bad.go.

Need to be documented in RELEASES.md?

No

@JonathanOppenheimer JonathanOppenheimer self-assigned this Nov 21, 2025
@JonathanOppenheimer JonathanOppenheimer added the testing This primarily focuses on testing label Nov 21, 2025
@JonathanOppenheimer JonathanOppenheimer marked this pull request as ready for review November 21, 2025 23:37
@@ -0,0 +1,49 @@
# Allowed libevm packages that can be imported in the graft directory.
Copy link
Contributor

Choose a reason for hiding this comment

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

grafts/scripts should only be used for graft-related functionality and is likely to go away at some point once we've migrated everything we care about.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this should also be vms/evm - I'll generalize to EVM code?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, vms/evm is a likely candidate. A key consideration of where to locate code is who will own it (i.e. be capable of approving changes). No point in requiring non-evm maintainers to approve evm code.

Copy link
Member Author

Choose a reason for hiding this comment

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

agree!

@@ -0,0 +1,232 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please find somewhere else for this too, the root of tests/ is for utility functions that get imported, not unit tests like this one.

Copy link
Member Author

Choose a reason for hiding this comment

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

yup, vm/evm also seems appropriate

@JonathanOppenheimer JonathanOppenheimer requested review from maru-ava and removed request for maru-ava November 24, 2025 19:15
@JonathanOppenheimer JonathanOppenheimer marked this pull request as draft November 24, 2025 20:13
@JonathanOppenheimer JonathanOppenheimer marked this pull request as ready for review November 24, 2025 23:06
)

// TestDoNotImportFromGraft ensures proper import rules for graft packages:
// - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception)
Copy link
Contributor

Choose a reason for hiding this comment

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

Myabe provide more detail as to why these restrictions are necessary?

return true
}

isInEmulate := strings.Contains(path, "/vms/evm/emulate/")
Copy link
Contributor

Choose a reason for hiding this comment

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

(No action required) Maybe localize the following 3 definitions to the conditionals that use them?

isInEmulate := strings.Contains(path, "/vms/evm/emulate/")
isInVmsEvm := strings.Contains(path, "/vms/evm/")
isCoreth := strings.Contains(importPath, "/graft/coreth")
isSubnetEVM := strings.Contains(importPath, "/graft/subnet-evm")
Copy link
Contributor

Choose a reason for hiding this comment

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

(No action required) Not a biggie, but would prefer to see subnet-evm specific stuff as part of the migration not in advance.

But also, why is testing for these independent grafts conflated? Maybe implement 2 tests that use the same helpers, one for each graft location?

require.Fail(t, formatImportViolations(foundImports, header))
}

// TestDoNotImportLibevmPseudo ensures that no code in the repository imports
Copy link
Contributor

Choose a reason for hiding this comment

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

(No action required) If it's libevm-internal only, why isn't the package called internal so that the restriction is enforced automatically by golang?

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package tests
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
package tests
package evm

Comment on lines +24 to +25
// - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception)
// - graft/subnet-evm cannot be imported anywhere EXCEPT vms/evm/emulate
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these the rules? I understand emulate being allowed to import, but not the other limitations.

Rule of thumb for all code, not just this instance: comments should explain "why", not "what" nor "how".

// - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception)
// - graft/subnet-evm cannot be imported anywhere EXCEPT vms/evm/emulate
func TestEnforceGraftImportBoundaries(t *testing.T) {
graftRegex := regexp.MustCompile(`^github\.com/ava-labs/avalanchego/graft(/|$)`)
Copy link
Contributor

Choose a reason for hiding this comment

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

(subjective, optional) I've seen enough bugs arising from regular expressions that could have been string comparisons that I tend to avoid them unless absolutely necessary. The functionality in this file could just be:

func isPackageIn(importPath, root string) bool {
	if importPath == root {
		return true
	}
	if len(importPath) < len(root)+1 {
		return false
	}
	return strings.HasPrefix(importPath, root) && importPath[len(root)] == '/'
}


// findImportsMatchingPattern is a generalized function that finds all imports
// matching a given regex pattern in the specified directory.
// The filterFunc can be used to skip certain files or imports (return true to skip).
Copy link
Contributor

Choose a reason for hiding this comment

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

return true to skip

This is the opposite to what I'd expect when passing in a filter function:

func myFilter(file string) bool {
   return true // intuition says this is now included
}

Comment on lines +131 to +133
if err != nil || !strings.HasSuffix(path, ".go") {
return err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if err != nil || !strings.HasSuffix(path, ".go") {
return err
}
if err != nil {
return err
}
if !strings.HasSuffix(path, ".go") {
return nil
}

Although functionally equivalent, this is much clearer and doesn't force the reader to reason about how the returned err will be nil in the non-Go instance.


// TestDoNotImportLibevmPseudo ensures that no code in the repository imports
// the libevm/pseudo package, which is for internal libevm use only.
func TestDoNotImportLibevmPseudo(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func TestDoNotImportLibevmPseudo(t *testing.T) {
func TestDoNotImportLibEVMPseudo(t *testing.T) {

Although the project name is stylised libevm, initialisms in Go identifiers should have all initials with the same capitalisation.


// TestDoNotImportLibevmPseudo ensures that no code in the repository imports
// the libevm/pseudo package, which is for internal libevm use only.
func TestDoNotImportLibevmPseudo(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this warrant a separate test?

func findImportsMatchingPattern(
rootDir string,
importRegex *regexp.Regexp,
filterFunc func(filePath string, importPath string, importSpec *ast.ImportSpec) bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
filterFunc func(filePath string, importPath string, importSpec *ast.ImportSpec) bool,
filterFunc func(filePath string, importPath string) bool,

It's never used.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've added comments based on the current macro-pattern, but that being said I think it's been overcomplicated by an unnecessary abstraction to support two tests. There's also no need to collect and then format the output when a t.Error() will do.

Comment on lines +34 to +35
strings.Contains(path, "graft/*/core/main_test.go") ||
strings.Contains(path, "graft/*/tempextrastest/") {
Copy link
Contributor

Choose a reason for hiding this comment

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

strings.Contains() doesn't support wild-card matching, but this is captured by the check below anyway.

@github-project-automation github-project-automation bot moved this to In Progress 🏗️ in avalanchego Nov 25, 2025
foundImports, err := findImportsMatchingPattern("..", graftRegex, func(path string, importPath string, _ *ast.ImportSpec) bool {
// Skip generated files and test-specific files
filename := filepath.Base(path)
if strings.HasPrefix(filename, "gen_") ||
Copy link
Contributor

@ARR4N ARR4N Nov 25, 2025

Choose a reason for hiding this comment

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

@ARR4N
Copy link
Contributor

ARR4N commented Nov 25, 2025

I think the entirety of this PR can be reduced to something like this:

func TestImportViolations(t *testing.T) {
	const root ".."
	err := filepath.Walk(root, func(file string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if strings.ToLower(filepath.Ext(file)) != "go" {
			return nil
		}

		node, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ImportsOnly)
		if err != nil {
			return fmt.Errorf("parser.ParseFile(..., %q, ...): %v", file, err)
		}

		for _, spec := range node.Imports {
			if spec.Path == nil {
				continue
			}
			imp := strings.Trim(spec.Path.Value, `"`)

			inGraft := strings.Contains(file, "/graft/")
			inEmulate := strings.Contains(file, "/vms/evm/emulate/")
			inVMsEVM := strings.Contains(file, "/vms/evm/")
			importsPseudo := isPackageIn(imp, "github.com/ava-labs/libevm/libevm/pseudo")
			importsCoreth := isPackageIn(imp, "github.com/ava-labs/avalanchego/graft/coreth")
			importsSubnetEVM := isPackageIn(imp, "github.com/ava-labs/avalanchego/graft/subnet-evm")

			// TODO: before merging, this needs a comment explaining why each of them are a violation
			violations := []bool{
				importsPseudo,
				!inGraft && importsCoreth && inVMsEVM && !inEmulate,
				!inGraft && importsSubnetEVM && !inEmulate,
			}
			if slices.Contains(violations, true) {
				t.Errorf("File %q imports %q", file, imp)
			}
		}
		return nil
	})

	require.NoErrorf(t, err, "filepath.Walk(%q)", root)
}

func isPackageIn(importPath, root string) bool {
	if importPath == root {
		return true
	}
	if len(importPath) < len(root)+1 {
		return false
	}
	return strings.HasPrefix(importPath, root) && importPath[len(root)] == '/'
}

I'm not particularly happy with the relative path to walk nor the way of checking if something is in a sub-dir (they can be fixed together), but otherwise I think this covers all bases.

@github-actions github-actions bot removed the monorepo label Nov 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testing This primarily focuses on testing

Projects

Status: In Progress 🏗️

Development

Successfully merging this pull request may close these issues.

4 participants