forked from ethereum-optimism/optimism
-
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.
feat: Rewrite natspec checker in Go (ethereum-optimism#12191)
* feat: Rewrite natspec checker in Go Rewrites the `semver-natspec-check-no-build` Just command in Go to reduce runtime. This PR reduces runtime for this check from ~1m30s to about 3 seconds post-compilation. * remove old script * add unit tests * rename test * review updates
- Loading branch information
Showing
5 changed files
with
343 additions
and
75 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
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
74 changes: 0 additions & 74 deletions
74
packages/contracts-bedrock/scripts/checks/check-semver-natspec-match.sh
This file was deleted.
Oops, something went wrong.
215 changes: 215 additions & 0 deletions
215
packages/contracts-bedrock/scripts/checks/semver-natspec/main.go
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,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 := filepath.Join(cwd, "src") | ||
|
||
artifactFiles, err := glob(artifactsDir, ".json") | ||
if err != nil { | ||
return fmt.Errorf("failed to get artifact files: %w", err) | ||
} | ||
contractFiles, err := glob(srcDir, ".sol") | ||
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") | ||
} | ||
|
||
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 !info.IsDir() && filepath.Ext(path) == ext { | ||
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 "" | ||
} |
Oops, something went wrong.