Commit aa81f074 authored by Michael Amadi's avatar Michael Amadi Committed by GitHub

transition semver-lock script to common framework (#13399)

parent 0d0f69d5
...@@ -5,124 +5,112 @@ import ( ...@@ -5,124 +5,112 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath"
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
) )
const semverLockFile = "snapshots/semver-lock.json" const semverLockFile = "snapshots/semver-lock.json"
func main() { type SemverLockOutput struct {
if err := run(); err != nil { InitCodeHash string `json:"initCodeHash"`
panic(err) SourceCodeHash string `json:"sourceCodeHash"`
} }
type SemverLockResult struct {
SemverLockOutput
SourceFilePath string
} }
func run() error { func main() {
// Find semver files results, err := common.ProcessFilesGlob(
// Execute grep command to find files with @custom:semver []string{"forge-artifacts/**/*.json"},
var cmd = exec.Command("bash", "-c", "grep -rl '@custom:semver' src | jq -Rs 'split(\"\n\") | map(select(length > 0))'") []string{},
cmdOutput, err := cmd.Output() processFile,
)
if err != nil { if err != nil {
return err fmt.Printf("error: %v\n", err)
os.Exit(1)
} }
// Parse the JSON array of files // Create the output map
var files []string output := make(map[string]SemverLockOutput)
if err := json.Unmarshal(cmdOutput, &files); err != nil { for _, result := range results {
return fmt.Errorf("failed to parse JSON output: %w", err) if result == nil {
continue
}
output[result.SourceFilePath] = result.SemverLockOutput
} }
// Hash and write to JSON file // Get and sort the keys
// Map to store our JSON output keys := make([]string, 0, len(output))
output := make(map[string]map[string]string) for k := range output {
keys = append(keys, k)
}
sort.Strings(keys)
// regex to extract contract name from file path // Create a sorted map for output
re := regexp.MustCompile(`src/.*/(.+)\.sol`) sortedOutput := make(map[string]SemverLockOutput)
for _, k := range keys {
sortedOutput[k] = output[k]
}
// Get artifacts directory // Write to JSON file
cmd = exec.Command("forge", "config", "--json") jsonData, err := json.MarshalIndent(sortedOutput, "", " ")
out, err := cmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to get forge config: %w", err) panic(err)
}
var config struct {
Out string `json:"out"`
} }
if err := json.Unmarshal(out, &config); err != nil { if err := os.WriteFile(semverLockFile, jsonData, 0644); err != nil {
return fmt.Errorf("failed to parse forge config: %w", err) panic(err)
} }
for _, file := range files { fmt.Printf("Wrote semver lock file to \"%s\".\n", semverLockFile)
// Read file contents }
fileContents, err := os.ReadFile(file)
func processFile(file string) (*SemverLockResult, []error) {
artifact, err := common.ReadForgeArtifact(file)
if err != nil { if err != nil {
return fmt.Errorf("failed to read file %s: %w", file, err) return nil, []error{fmt.Errorf("failed to read artifact: %w", err)}
} }
// Extract contract name from file path using regex // Only apply to files in the src directory.
matches := re.FindStringSubmatch(file) sourceFilePath := artifact.Ast.AbsolutePath
if len(matches) < 2 { if !strings.HasPrefix(sourceFilePath, "src/") {
return fmt.Errorf("invalid file path format: %s", file) return nil, nil
} }
contractName := matches[1]
// Get artifact files // Check if the contract uses semver.
artifactDir := filepath.Join(config.Out, contractName+".sol") semverRegex := regexp.MustCompile(`custom:semver`)
files, err := os.ReadDir(artifactDir) semver := semverRegex.FindStringSubmatch(artifact.RawMetadata)
if err != nil { if len(semver) == 0 {
return fmt.Errorf("failed to read artifact directory: %w", err) return nil, nil
}
if len(files) == 0 {
return fmt.Errorf("no artifacts found for %s", contractName)
} }
// Read initcode from artifact // Extract the init code from the artifact.
artifactPath := filepath.Join(artifactDir, files[0].Name()) initCodeBytes, err := hex.DecodeString(strings.TrimPrefix(artifact.Bytecode.Object, "0x"))
artifact, err := os.ReadFile(artifactPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to read initcode: %w", err) return nil, []error{fmt.Errorf("failed to decode hex: %w", err)}
}
artifactJson := json.RawMessage(artifact)
var artifactObj struct {
Bytecode struct {
Object string `json:"object"`
} `json:"bytecode"`
}
if err := json.Unmarshal(artifactJson, &artifactObj); err != nil {
return fmt.Errorf("failed to parse artifact: %w", err)
} }
// convert the hex bytecode to a uint8 array / bytes // Extract the source contents from the AST.
initCodeBytes, err := hex.DecodeString(strings.TrimPrefix(artifactObj.Bytecode.Object, "0x")) sourceCode, err := os.ReadFile(sourceFilePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to decode hex: %w", err) return nil, []error{fmt.Errorf("failed to read source file: %w", err)}
} }
// Calculate hashes using Keccak256 // Calculate hashes using Keccak256
var sourceCode = []byte(strings.TrimSuffix(string(fileContents), "\n")) trimmedSourceCode := []byte(strings.TrimSuffix(string(sourceCode), "\n"))
initCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(initCodeBytes)) initCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(initCodeBytes))
sourceCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(sourceCode)) sourceCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(trimmedSourceCode))
// Store in output map return &SemverLockResult{
output[file] = map[string]string{ SourceFilePath: sourceFilePath,
"initCodeHash": initCodeHash, SemverLockOutput: SemverLockOutput{
"sourceCodeHash": sourceCodeHash, InitCodeHash: initCodeHash,
} SourceCodeHash: sourceCodeHash,
} },
}, nil
// Write to JSON file
jsonData, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
if err := os.WriteFile(semverLockFile, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write semver lock file: %w", err)
}
fmt.Printf("Wrote semver lock file to \"%s\".\n", semverLockFile)
return nil
} }
...@@ -37,20 +37,27 @@ func (e *ErrorReporter) HasError() bool { ...@@ -37,20 +37,27 @@ func (e *ErrorReporter) HasError() bool {
return e.hasErr.Load() return e.hasErr.Load()
} }
type FileProcessor func(path string) []error type Void struct{}
func ProcessFiles(files map[string]string, processor FileProcessor) error { type FileProcessor[T any] func(path string) (T, []error)
func ProcessFiles[T any](files map[string]string, processor FileProcessor[T]) (map[string]T, error) {
g := errgroup.Group{} g := errgroup.Group{}
g.SetLimit(runtime.NumCPU()) g.SetLimit(runtime.NumCPU())
reporter := NewErrorReporter() reporter := NewErrorReporter()
results := sync.Map{}
for name, path := range files { for name, path := range files {
name, path := name, path // Capture loop variables name, path := name, path // Capture loop variables
g.Go(func() error { g.Go(func() error {
if errs := processor(path); len(errs) > 0 { result, errs := processor(path)
if len(errs) > 0 {
for _, err := range errs { for _, err := range errs {
reporter.Fail("%s: %v", name, err) reporter.Fail("%s: %v", name, err)
} }
} else {
results.Store(path, result)
} }
return nil return nil
}) })
...@@ -58,18 +65,26 @@ func ProcessFiles(files map[string]string, processor FileProcessor) error { ...@@ -58,18 +65,26 @@ func ProcessFiles(files map[string]string, processor FileProcessor) error {
err := g.Wait() err := g.Wait()
if err != nil { if err != nil {
return fmt.Errorf("processing failed: %w", err) return nil, fmt.Errorf("processing failed: %w", err)
} }
if reporter.HasError() { if reporter.HasError() {
return fmt.Errorf("processing failed") return nil, fmt.Errorf("processing failed")
} }
return nil
// Convert sync.Map to regular map
finalResults := make(map[string]T)
results.Range(func(key, value interface{}) bool {
finalResults[key.(string)] = value.(T)
return true
})
return finalResults, nil
} }
func ProcessFilesGlob(includes, excludes []string, processor FileProcessor) error { func ProcessFilesGlob[T any](includes, excludes []string, processor FileProcessor[T]) (map[string]T, error) {
files, err := FindFiles(includes, excludes) files, err := FindFiles(includes, excludes)
if err != nil { if err != nil {
return err return nil, err
} }
return ProcessFiles(files, processor) return ProcessFiles(files, processor)
} }
......
package common package common
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
...@@ -33,23 +34,55 @@ func TestProcessFiles(t *testing.T) { ...@@ -33,23 +34,55 @@ func TestProcessFiles(t *testing.T) {
"file2": "path2", "file2": "path2",
} }
// Test successful processing // Test void processing (no results)
err := ProcessFiles(files, func(path string) []error { _, err := ProcessFiles(files, func(path string) (*Void, []error) {
return nil return nil, nil
}) })
if err != nil { if err != nil {
t.Errorf("expected no error, got %v", err) t.Errorf("expected no error, got %v", err)
} }
// Test error handling // Test error handling
err = ProcessFiles(files, func(path string) []error { _, err = ProcessFiles(files, func(path string) (*Void, []error) {
var errors []error var errors []error
errors = append(errors, os.ErrNotExist) errors = append(errors, os.ErrNotExist)
return errors return nil, errors
}) })
if err == nil { if err == nil {
t.Error("expected error, got nil") t.Error("expected error, got nil")
} }
// Test successful processing with string results
results, err := ProcessFiles(files, func(path string) (string, []error) {
return "processed_" + path, nil
})
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if len(results) != 2 {
t.Errorf("expected 2 results, got %d", len(results))
}
if results["path1"] != "processed_path1" {
t.Errorf("expected processed_path1, got %s", results["path1"])
}
// Test processing with struct results
type testResult struct {
Path string
Counter int
}
structResults, err := ProcessFiles(files, func(path string) (testResult, []error) {
return testResult{Path: path, Counter: len(path)}, nil
})
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if len(structResults) != 2 {
t.Errorf("expected 2 results, got %d", len(structResults))
}
if structResults["path1"].Counter != 5 {
t.Errorf("expected counter 5, got %d", structResults["path1"].Counter)
}
} }
func TestProcessFilesGlob(t *testing.T) { func TestProcessFilesGlob(t *testing.T) {
...@@ -75,24 +108,24 @@ func TestProcessFilesGlob(t *testing.T) { ...@@ -75,24 +108,24 @@ func TestProcessFilesGlob(t *testing.T) {
} }
} }
// Test processing with includes and excludes
includes := []string{"*.txt"} includes := []string{"*.txt"}
excludes := []string{"skip.txt"} excludes := []string{"skip.txt"}
// Test void processing (no results)
processedFiles := make(map[string]bool) processedFiles := make(map[string]bool)
var mtx sync.Mutex var mtx sync.Mutex
err := ProcessFilesGlob(includes, excludes, func(path string) []error { _, err := ProcessFilesGlob(includes, excludes, func(path string) (*Void, []error) {
mtx.Lock() mtx.Lock()
processedFiles[filepath.Base(path)] = true processedFiles[filepath.Base(path)] = true
mtx.Unlock() mtx.Unlock()
return nil return nil, nil
}) })
if err != nil { if err != nil {
t.Errorf("ProcessFiles failed: %v", err) t.Errorf("ProcessFiles failed: %v", err)
} }
// Verify results // Verify void processing results
if len(processedFiles) != 2 { if len(processedFiles) != 2 {
t.Errorf("expected 2 processed files, got %d", len(processedFiles)) t.Errorf("expected 2 processed files, got %d", len(processedFiles))
} }
...@@ -105,6 +138,54 @@ func TestProcessFilesGlob(t *testing.T) { ...@@ -105,6 +138,54 @@ func TestProcessFilesGlob(t *testing.T) {
if processedFiles["skip.txt"] { if processedFiles["skip.txt"] {
t.Error("skip.txt should have been excluded") t.Error("skip.txt should have been excluded")
} }
// Test processing with struct results
type fileInfo struct {
Size int64
Content string
}
results, err := ProcessFilesGlob(includes, excludes, func(path string) (fileInfo, []error) {
content, err := os.ReadFile(path)
if err != nil {
return fileInfo{}, []error{err}
}
info, err := os.Stat(path)
if err != nil {
return fileInfo{}, []error{err}
}
return fileInfo{
Size: info.Size(),
Content: string(content),
}, nil
})
if err != nil {
t.Errorf("ProcessFilesGlob failed: %v", err)
}
// Verify struct results
if len(results) != 2 {
t.Errorf("expected 2 results, got %d", len(results))
}
if result, exists := results["test1.txt"]; !exists {
t.Error("expected result for test1.txt")
} else {
if result.Content != "content1" {
t.Errorf("expected content1, got %s", result.Content)
}
if result.Size != 8 {
t.Errorf("expected size 8, got %d", result.Size)
}
}
// Test error handling
_, err = ProcessFilesGlob(includes, excludes, func(path string) (fileInfo, []error) {
return fileInfo{}, []error{fmt.Errorf("test error")}
})
if err == nil {
t.Error("expected error, got nil")
}
} }
func TestFindFiles(t *testing.T) { func TestFindFiles(t *testing.T) {
......
...@@ -50,7 +50,7 @@ type Artifact struct { ...@@ -50,7 +50,7 @@ type Artifact struct {
} }
func main() { func main() {
if err := common.ProcessFilesGlob( if _, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"}, []string{"forge-artifacts/**/*.json"},
[]string{}, []string{},
processFile, processFile,
...@@ -60,56 +60,56 @@ func main() { ...@@ -60,56 +60,56 @@ func main() {
} }
} }
func processFile(artifactPath string) []error { func processFile(artifactPath string) (*common.Void, []error) {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to get current working directory: %w", err)} return nil, []error{fmt.Errorf("failed to get current working directory: %w", err)}
} }
artifactsDir := filepath.Join(cwd, "forge-artifacts") artifactsDir := filepath.Join(cwd, "forge-artifacts")
contractName := strings.Split(filepath.Base(artifactPath), ".")[0] contractName := strings.Split(filepath.Base(artifactPath), ".")[0]
if isExcluded(contractName) { if isExcluded(contractName) {
return nil return nil, nil
} }
artifact, err := readArtifact(artifactPath) artifact, err := readArtifact(artifactPath)
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to read artifact: %w", err)} return nil, []error{fmt.Errorf("failed to read artifact: %w", err)}
} }
contractDef := getContractDefinition(artifact, contractName) contractDef := getContractDefinition(artifact, contractName)
if contractDef == nil { if contractDef == nil {
return nil // Skip processing if contract definition is not found return nil, nil // Skip processing if contract definition is not found
} }
if contractDef.ContractKind != "interface" { if contractDef.ContractKind != "interface" {
return nil return nil, nil
} }
if !strings.HasPrefix(contractName, "I") { if !strings.HasPrefix(contractName, "I") {
return []error{fmt.Errorf("%s: Interface does not start with 'I'", contractName)} return nil, []error{fmt.Errorf("%s: Interface does not start with 'I'", contractName)}
} }
semver, err := getContractSemver(artifact) semver, err := getContractSemver(artifact)
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to get contract semver: %w", err)} return nil, []error{fmt.Errorf("failed to get contract semver: %w", err)}
} }
if semver != "solidity^0.8.0" { if semver != "solidity^0.8.0" {
return []error{fmt.Errorf("%s: Interface does not have correct compiler version (MUST be exactly solidity ^0.8.0)", contractName)} return nil, []error{fmt.Errorf("%s: Interface does not have correct compiler version (MUST be exactly solidity ^0.8.0)", contractName)}
} }
contractBasename := contractName[1:] contractBasename := contractName[1:]
correspondingContractFile := filepath.Join(artifactsDir, contractBasename+".sol", contractBasename+".json") correspondingContractFile := filepath.Join(artifactsDir, contractBasename+".sol", contractBasename+".json")
if _, err := os.Stat(correspondingContractFile); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(correspondingContractFile); errors.Is(err, os.ErrNotExist) {
return nil return nil, nil
} }
contractArtifact, err := readArtifact(correspondingContractFile) contractArtifact, err := readArtifact(correspondingContractFile)
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to read corresponding contract artifact: %w", err)} return nil, []error{fmt.Errorf("failed to read corresponding contract artifact: %w", err)}
} }
interfaceABI := artifact.ABI interfaceABI := artifact.ABI
...@@ -117,23 +117,23 @@ func processFile(artifactPath string) []error { ...@@ -117,23 +117,23 @@ func processFile(artifactPath string) []error {
normalizedInterfaceABI, err := normalizeABI(interfaceABI) normalizedInterfaceABI, err := normalizeABI(interfaceABI)
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to normalize interface ABI: %w", err)} return nil, []error{fmt.Errorf("failed to normalize interface ABI: %w", err)}
} }
normalizedContractABI, err := normalizeABI(contractABI) normalizedContractABI, err := normalizeABI(contractABI)
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to normalize contract ABI: %w", err)} return nil, []error{fmt.Errorf("failed to normalize contract ABI: %w", err)}
} }
match, err := compareABIs(normalizedInterfaceABI, normalizedContractABI) match, err := compareABIs(normalizedInterfaceABI, normalizedContractABI)
if err != nil { if err != nil {
return []error{fmt.Errorf("failed to compare ABIs: %w", err)} return nil, []error{fmt.Errorf("failed to compare ABIs: %w", err)}
} }
if !match { if !match {
return []error{fmt.Errorf("%s: Differences found in ABI between interface and actual contract", contractName)} return nil, []error{fmt.Errorf("%s: Differences found in ABI between interface and actual contract", contractName)}
} }
return nil return nil, nil
} }
func readArtifact(path string) (*Artifact, error) { func readArtifact(path string) (*Artifact, error) {
......
...@@ -89,14 +89,14 @@ func validateSpacer(variable solc.StorageLayoutEntry, types map[string]solc.Stor ...@@ -89,14 +89,14 @@ func validateSpacer(variable solc.StorageLayoutEntry, types map[string]solc.Stor
return errors return errors
} }
func processFile(path string) []error { func processFile(path string) (*common.Void, []error) {
artifact, err := common.ReadForgeArtifact(path) artifact, err := common.ReadForgeArtifact(path)
if err != nil { if err != nil {
return []error{err} return nil, []error{err}
} }
if artifact.StorageLayout == nil { if artifact.StorageLayout == nil {
return nil return nil, nil
} }
var errors []error var errors []error
...@@ -109,11 +109,11 @@ func processFile(path string) []error { ...@@ -109,11 +109,11 @@ func processFile(path string) []error {
} }
} }
return errors return nil, errors
} }
func main() { func main() {
if err := common.ProcessFilesGlob( if _, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"}, []string{"forge-artifacts/**/*.json"},
[]string{"forge-artifacts/**/CrossDomainMessengerLegacySpacer{0,1}.json"}, []string{"forge-artifacts/**/CrossDomainMessengerLegacySpacer{0,1}.json"},
processFile, processFile,
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
) )
func main() { func main() {
if err := common.ProcessFilesGlob( if _, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"}, []string{"forge-artifacts/**/*.json"},
[]string{}, []string{},
processFile, processFile,
...@@ -22,10 +22,10 @@ func main() { ...@@ -22,10 +22,10 @@ func main() {
} }
} }
func processFile(path string) []error { func processFile(path string) (*common.Void, []error) {
artifact, err := common.ReadForgeArtifact(path) artifact, err := common.ReadForgeArtifact(path)
if err != nil { if err != nil {
return []error{err} return nil, []error{err}
} }
var errors []error var errors []error
...@@ -36,7 +36,7 @@ func processFile(path string) []error { ...@@ -36,7 +36,7 @@ func processFile(path string) []error {
} }
} }
return errors return nil, errors
} }
func extractTestNames(artifact *solc.ForgeArtifact) []string { func extractTestNames(artifact *solc.ForgeArtifact) []string {
......
...@@ -14,7 +14,7 @@ var importPattern = regexp.MustCompile(`import\s*{([^}]+)}`) ...@@ -14,7 +14,7 @@ var importPattern = regexp.MustCompile(`import\s*{([^}]+)}`)
var asPattern = regexp.MustCompile(`(\S+)\s+as\s+(\S+)`) var asPattern = regexp.MustCompile(`(\S+)\s+as\s+(\S+)`)
func main() { func main() {
if err := common.ProcessFilesGlob( if _, err := common.ProcessFilesGlob(
[]string{"src/**/*.sol", "scripts/**/*.sol", "test/**/*.sol"}, []string{"src/**/*.sol", "scripts/**/*.sol", "test/**/*.sol"},
[]string{}, []string{},
processFile, processFile,
...@@ -24,10 +24,10 @@ func main() { ...@@ -24,10 +24,10 @@ func main() {
} }
} }
func processFile(filePath string) []error { func processFile(filePath string) (*common.Void, []error) {
content, err := os.ReadFile(filePath) content, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return []error{fmt.Errorf("%s: failed to read file: %w", filePath, err)} return nil, []error{fmt.Errorf("%s: failed to read file: %w", filePath, err)}
} }
imports := findImports(string(content)) imports := findImports(string(content))
...@@ -43,10 +43,10 @@ func processFile(filePath string) []error { ...@@ -43,10 +43,10 @@ func processFile(filePath string) []error {
for _, unused := range unusedImports { for _, unused := range unusedImports {
errors = append(errors, fmt.Errorf("%s", unused)) errors = append(errors, fmt.Errorf("%s", unused))
} }
return errors return nil, errors
} }
return nil return nil, nil
} }
func findImports(content string) []string { func findImports(content string) []string {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment