Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Rewrite natspec checker in Go #12191

Merged
merged 5 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,9 @@ workflows:
- op-program
- op-service
- op-supervisor
- go-test:
name: semver-natspec-tests
module: packages/contracts-bedrock/scripts/checks/semver-natspec
- go-test-kurtosis:
name: op-chain-ops-integration
module: op-chain-ops
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts-bedrock/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ semver-diff-check: build semver-diff-check-no-build
# Checks that semver natspec is equal to the actual semver version.
# Does not build contracts.
semver-natspec-check-no-build:
./scripts/checks/check-semver-natspec-match.sh
mslipper marked this conversation as resolved.
Show resolved Hide resolved
go run ./scripts/checks/semver-natspec

# Checks that semver natspec is equal to the actual semver version.
semver-natspec-check: build semver-natspec-check-no-build
Expand Down

This file was deleted.

215 changes: 215 additions & 0 deletions packages/contracts-bedrock/scripts/checks/semver-natspec/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package main

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"sync/atomic"
)

type ArtifactsWrapper struct {
RawMetadata string `json:"rawMetadata"`
}

type Artifacts struct {
Output struct {
Devdoc struct {
StateVariables struct {
Version struct {
Semver string `json:"custom:semver"`
} `json:"version"`
} `json:"stateVariables,omitempty"`
Methods struct {
Version struct {
Semver string `json:"custom:semver"`
} `json:"version()"`
} `json:"methods,omitempty"`
} `json:"devdoc"`
} `json:"output"`
}

var ConstantVersionPattern = regexp.MustCompile(`string.*constant.*version\s+=\s+"([^"]+)";`)

var FunctionVersionPattern = regexp.MustCompile(`^\s+return\s+"((?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)";$`)

var InteropVersionPattern = regexp.MustCompile(`^\s+return\s+string\.concat\(super\.version\(\), "((.*)\+interop(.*)?)"\);`)

func main() {
if err := run(); err != nil {
writeStderr("an error occurred: %v", err)
os.Exit(1)
}
}

func writeStderr(msg string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, msg+"\n", args...)
}

func run() error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}

writeStderr("working directory: %s", cwd)

artifactsDir := filepath.Join(cwd, "forge-artifacts")
srcDir := cwd

artifactFiles, err := glob(artifactsDir, ".json")
if err != nil {
return fmt.Errorf("failed to get artifact files: %w", err)
}
contractFiles, err := glob(srcDir, ".sol")
mslipper marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("failed to get contract files: %w", err)
}

var hasErr int32
var outMtx sync.Mutex
fail := func(msg string, args ...any) {
outMtx.Lock()
writeStderr("❌ "+msg, args...)
outMtx.Unlock()
atomic.StoreInt32(&hasErr, 1)
}

sem := make(chan struct{}, runtime.NumCPU())
for contractName, artifactPath := range artifactFiles {
contractName := contractName
artifactPath := artifactPath

sem <- struct{}{}

go func() {
defer func() {
<-sem
}()

af, err := os.Open(artifactPath)
if err != nil {
fail("%s: failed to open contract artifact: %v", contractName, err)
return
}
defer af.Close()

var wrapper ArtifactsWrapper
if err := json.NewDecoder(af).Decode(&wrapper); err != nil {
fail("%s: failed to parse artifact file: %v", contractName, err)
return
}

if wrapper.RawMetadata == "" {
return
}

var artifactData Artifacts
if err := json.Unmarshal([]byte(wrapper.RawMetadata), &artifactData); err != nil {
fail("%s: failed to unwrap artifact metadata: %v", contractName, err)
return
}

artifactVersion := artifactData.Output.Devdoc.StateVariables.Version.Semver

isConstant := true
if artifactData.Output.Devdoc.StateVariables.Version.Semver == "" {
artifactVersion = artifactData.Output.Devdoc.Methods.Version.Semver
isConstant = false
}

if artifactVersion == "" {
return
}

contractPath := contractFiles[contractName]
if contractPath == "" {
fail("%s: Source file not found", contractName)
return
}

cf, err := os.Open(contractPath)
if err != nil {
fail("%s: failed to open contract source: %v", contractName, err)
return
}
defer cf.Close()

sourceData, err := io.ReadAll(cf)
if err != nil {
fail("%s: failed to read contract source: %v", contractName, err)
return
}

var sourceVersion string

if isConstant {
sourceVersion = findLine(sourceData, ConstantVersionPattern)
} else {
sourceVersion = findLine(sourceData, FunctionVersionPattern)
}

// Need to define a special case for interop contracts since they technically
// use an invalid semver format. Checking for sourceVersion == "" allows the
// team to update the format to a valid semver format in the future without
// needing to change this program.
if sourceVersion == "" && strings.HasSuffix(contractName, "Interop") {
sourceVersion = findLine(sourceData, InteropVersionPattern)
}

if sourceVersion == "" {
fail("%s: version not found in source", contractName)
return
}

if sourceVersion != artifactVersion {
fail("%s: version mismatch: source=%s, artifact=%s", contractName, sourceVersion, artifactVersion)
return
}

_, _ = fmt.Fprintf(os.Stderr, "✅ %s: code: %s, artifact: %s\n", contractName, sourceVersion, artifactVersion)
}()
}

for i := 0; i < cap(sem); i++ {
sem <- struct{}{}
}

if atomic.LoadInt32(&hasErr) == 1 {
return fmt.Errorf("semver check failed, see logs above")
mslipper marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}

func glob(dir string, ext string) (map[string]string, error) {
out := make(map[string]string)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) == ext {
mslipper marked this conversation as resolved.
Show resolved Hide resolved
out[strings.TrimSuffix(filepath.Base(path), ext)] = path
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk directory: %w", err)
}
return out, nil
}

func findLine(in []byte, pattern *regexp.Regexp) string {
scanner := bufio.NewScanner(bytes.NewReader(in))
for scanner.Scan() {
match := pattern.FindStringSubmatch(scanner.Text())
if len(match) > 0 {
return match[1]
}
}
return ""
}
Loading