Commit 5e317379 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

contracts-bedrock: remove typescript (#11340)

* contracts-bedrock: remove typescript

Moves to using Go from typescript, new code is autogenerated
by claude. Can confirm that both work as expected.

The `check-test-names` is not running in CI, it fails for both
the Go and Typescript scripts. Maybe claude will be able to get
the script to pass.

* Update check-spacers.go
Co-authored-by: default avatarsemgrep-app[bot] <63493438+semgrep-app[bot]@users.noreply.github.com>

* Update packages/contracts-bedrock/scripts/checks/check-test-names.go
Co-authored-by: default avatarsemgrep-app[bot] <63493438+semgrep-app[bot]@users.noreply.github.com>

* contracts-bedrock: fix build

* scripts: rename

* scripts: rename

* scripts: fixes

---------
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
Co-authored-by: default avatarsemgrep-app[bot] <63493438+semgrep-app[bot]@users.noreply.github.com>
parent 4c9811f2
......@@ -33,14 +33,14 @@
"snapshots:check": "./scripts/checks/check-snapshots.sh",
"semver-lock": "forge script scripts/SemverLock.s.sol",
"validate-deploy-configs": "./scripts/checks/check-deploy-configs.sh",
"validate-spacers:no-build": "npx tsx scripts/checks/check-spacers.ts",
"validate-spacers:no-build": "go run ./scripts/checks/spacers",
"validate-spacers": "pnpm build && pnpm validate-spacers:no-build",
"clean": "rm -rf ./artifacts ./forge-artifacts ./cache ./tsconfig.tsbuildinfo ./tsconfig.build.tsbuildinfo ./scripts/go-ffi/go-ffi ./.testdata ./deployments/hardhat/*",
"pre-pr:no-build": "pnpm gas-snapshot:no-build && pnpm snapshots && pnpm semver-lock && pnpm autogen:invariant-docs && pnpm lint",
"pre-pr": "pnpm clean && pnpm build:go-ffi && pnpm build && pnpm pre-pr:no-build",
"pre-pr:full": "pnpm test && pnpm validate-deploy-configs && pnpm validate-spacers && pnpm pre-pr",
"lint:ts:check": "eslint . --max-warnings=0",
"lint:forge-tests:check": "npx tsx scripts/checks/check-test-names.ts",
"lint:forge-tests:check": "go run ./scripts/checks/names",
"lint:contracts:check": "pnpm lint:fix && git diff --exit-code",
"lint:check": "pnpm lint:contracts:check && pnpm lint:ts:check",
"lint:ts:fix": "eslint --fix .",
......
import fs from 'fs'
import path from 'path'
/**
* Directory path to the artifacts.
* Can be configured as the first argument to the script or
* defaults to the forge-artifacts directory.
*/
const directoryPath =
process.argv[2] || path.join(__dirname, '..', '..', 'forge-artifacts')
/**
* Returns true if the contract should be skipped when inspecting its storage layout.
* This is useful for abstract contracts that are meant to be inherited.
* The two particular targets are:
* - CrossDomainMessengerLegacySpacer0
* - CrossDomainMessengerLegacySpacer1
*/
const skipped = (contractName: string): boolean => {
return contractName.includes('CrossDomainMessengerLegacySpacer')
}
/**
* Parses out variable info from the variable structure in standard compiler json output.
*
* @param variable Variable structure from standard compiler json output.
* @returns Parsed variable info.
*/
const parseVariableInfo = (
variable: any
): {
name: string
slot: number
offset: number
length: number
} => {
// Figure out the length of the variable.
let variableLength: number
if (variable.type.startsWith('t_mapping')) {
variableLength = 32
} else if (variable.type.startsWith('t_uint')) {
variableLength = variable.type.match(/uint([0-9]+)/)?.[1] / 8
} else if (variable.type.startsWith('t_bytes_')) {
variableLength = 32
} else if (variable.type.startsWith('t_bytes')) {
variableLength = parseInt(variable.type.match(/bytes([0-9]+)/)?.[1], 10)
} else if (variable.type.startsWith('t_address')) {
variableLength = 20
} else if (variable.type.startsWith('t_bool')) {
variableLength = 1
} else if (variable.type.startsWith('t_array')) {
// Figure out the size of the type inside of the array
// and then multiply that by the length of the array.
// This does not support recursion multiple times for simplicity
const type = variable.type.match(/^t_array\((\w+)\)/)?.[1]
const info = parseVariableInfo({
label: variable.label,
offset: variable.offset,
slot: variable.slot,
type,
})
const size = variable.type.match(/^t_array\(\w+\)([0-9]+)/)?.[1]
variableLength = info.length * parseInt(size, 10)
} else {
throw new Error(
`${variable.label}: unsupported type ${variable.type}, add it to the script`
)
}
return {
name: variable.label,
slot: parseInt(variable.slot, 10),
offset: variable.offset,
length: variableLength,
}
}
/**
* Main logic of the script
* - Ensures that all of the spacer variables are named correctly
*/
const main = async () => {
const paths = []
const readFilesRecursively = (dir: string) => {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const fileStat = fs.statSync(filePath)
if (fileStat.isDirectory()) {
readFilesRecursively(filePath)
} else {
paths.push(filePath)
}
}
}
readFilesRecursively(directoryPath)
for (const filePath of paths) {
if (filePath.includes('t.sol')) {
continue
}
const raw = fs.readFileSync(filePath, 'utf8').toString()
const artifact = JSON.parse(raw)
// Handle contracts without storage
const storageLayout = artifact.storageLayout || {}
if (storageLayout.storage) {
for (const variable of storageLayout.storage) {
const fqn = variable.contract
// Skip some abstract contracts
if (skipped(fqn)) {
continue
}
// Check that the spacers are all named correctly
if (variable.label.startsWith('spacer_')) {
const [, slot, offset, length] = variable.label.split('_')
const variableInfo = parseVariableInfo(variable)
// Check that the slot is correct.
if (parseInt(slot, 10) !== variableInfo.slot) {
throw new Error(
`${fqn} ${variable.label} is in slot ${variable.slot} but should be in ${slot}`
)
}
// Check that the offset is correct.
if (parseInt(offset, 10) !== variableInfo.offset) {
throw new Error(
`${fqn} ${variable.label} is at offset ${variable.offset} but should be at ${offset}`
)
}
// Check that the length is correct.
if (parseInt(length, 10) !== variableInfo.length) {
throw new Error(
`${fqn} ${variable.label} is ${variableInfo.length} bytes long but should be ${length}`
)
}
console.log(`${fqn}.${variable.label} is valid`)
}
}
}
}
}
main()
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
type Check = (parts: string[]) => boolean
type Checks = Array<{
check: Check
error: string
}>
/**
* Series of function name checks.
*/
const checks: Checks = [
{
error: 'test name parts should be in camelCase',
check: (parts: string[]): boolean => {
return parts.every((part) => {
return part[0] === part[0].toLowerCase()
})
},
},
{
error:
'test names should have either 3 or 4 parts, each separated by underscores',
check: (parts: string[]): boolean => {
return parts.length === 3 || parts.length === 4
},
},
{
error: 'test names should begin with "test", "testFuzz", or "testDiff"',
check: (parts: string[]): boolean => {
return ['test', 'testFuzz', 'testDiff'].includes(parts[0])
},
},
{
error:
'test names should end with either "succeeds", "reverts", "fails", "works" or "benchmark[_num]"',
check: (parts: string[]): boolean => {
return (
['succeeds', 'reverts', 'fails', 'benchmark', 'works'].includes(
parts[parts.length - 1]
) ||
(parts[parts.length - 2] === 'benchmark' &&
!isNaN(parseInt(parts[parts.length - 1], 10)))
)
},
},
{
error:
'failure tests should have 4 parts, third part should indicate the reason for failure',
check: (parts: string[]): boolean => {
return (
parts.length === 4 ||
!['reverts', 'fails'].includes(parts[parts.length - 1])
)
},
},
]
/**
* Script for checking that all test functions are named correctly.
*/
const main = async () => {
const result = execSync('forge config --json')
const config = JSON.parse(result.toString())
const out = config.out || 'out'
const paths = []
const readFilesRecursively = (dir: string) => {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const fileStat = fs.statSync(filePath)
if (fileStat.isDirectory()) {
readFilesRecursively(filePath)
} else {
paths.push(filePath)
}
}
}
readFilesRecursively(out)
console.log('Success:')
const errors: string[] = []
for (const filepath of paths) {
const artifact = JSON.parse(fs.readFileSync(filepath, 'utf8'))
let isTest = false
for (const element of artifact.abi) {
if (element.name === 'IS_TEST') {
isTest = true
break
}
}
if (isTest) {
let success = true
for (const element of artifact.abi) {
// Skip non-functions and functions that don't start with "test".
if (element.type !== 'function' || !element.name.startsWith('test')) {
continue
}
// Check the rest.
for (const { check, error } of checks) {
if (!check(element.name.split('_'))) {
errors.push(`${filepath}#${element.name}: ${error}`)
success = false
}
}
}
if (success) {
console.log(` - ${path.parse(filepath).name}`)
}
}
}
if (errors.length > 0) {
console.error(errors.join('\n'))
process.exit(1)
}
}
main()
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"unicode"
)
type Check func(parts []string) bool
type CheckInfo struct {
check Check
error string
}
var checks = []CheckInfo{
{
error: "test name parts should be in camelCase",
check: func(parts []string) bool {
for _, part := range parts {
if len(part) > 0 && unicode.IsUpper(rune(part[0])) {
return false
}
}
return true
},
},
{
error: "test names should have either 3 or 4 parts, each separated by underscores",
check: func(parts []string) bool {
return len(parts) == 3 || len(parts) == 4
},
},
{
error: "test names should begin with \"test\", \"testFuzz\", or \"testDiff\"",
check: func(parts []string) bool {
return parts[0] == "test" || parts[0] == "testFuzz" || parts[0] == "testDiff"
},
},
{
error: "test names should end with either \"succeeds\", \"reverts\", \"fails\", \"works\" or \"benchmark[_num]\"",
check: func(parts []string) bool {
last := parts[len(parts)-1]
if last == "succeeds" || last == "reverts" || last == "fails" || last == "works" {
return true
}
if len(parts) >= 2 && parts[len(parts)-2] == "benchmark" {
_, err := strconv.Atoi(last)
return err == nil
}
return last == "benchmark"
},
},
{
error: "failure tests should have 4 parts, third part should indicate the reason for failure",
check: func(parts []string) bool {
last := parts[len(parts)-1]
return len(parts) == 4 || (last != "reverts" && last != "fails")
},
},
}
func main() {
cmd := exec.Command("forge", "config", "--json")
output, err := cmd.Output()
if err != nil {
fmt.Printf("Error executing forge config: %v\n", err)
os.Exit(1)
}
var config map[string]interface{}
err = json.Unmarshal(output, &config)
if err != nil {
fmt.Printf("Error parsing forge config: %v\n", err)
os.Exit(1)
}
outDir, ok := config["out"].(string)
if !ok {
outDir = "out"
}
fmt.Println("Success:")
var errors []string
err = filepath.Walk(outDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var artifact map[string]interface{}
err = json.Unmarshal(data, &artifact)
if err != nil {
return nil // Skip files that are not valid JSON
}
abi, ok := artifact["abi"].([]interface{})
if !ok {
return nil
}
isTest := false
for _, element := range abi {
if elem, ok := element.(map[string]interface{}); ok {
if elem["name"] == "IS_TEST" {
isTest = true
break
}
}
}
if isTest {
success := true
for _, element := range abi {
if elem, ok := element.(map[string]interface{}); ok {
if elem["type"] == "function" {
name, ok := elem["name"].(string)
if !ok || !strings.HasPrefix(name, "test") {
continue
}
parts := strings.Split(name, "_")
for _, check := range checks {
if !check.check(parts) {
errors = append(errors, fmt.Sprintf("%s#%s: %s", path, name, check.error))
success = false
}
}
}
}
}
if success {
fmt.Printf(" - %s\n", filepath.Base(path[:len(path)-len(filepath.Ext(path))]))
}
}
return nil
})
if err != nil {
fmt.Printf("Error walking the path %q: %v\n", outDir, err)
os.Exit(1)
}
if len(errors) > 0 {
fmt.Println(strings.Join(errors, "\n"))
os.Exit(1)
}
}
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// directoryPath is the path to the artifacts directory.
// It can be configured as the first argument to the script or
// defaults to the forge-artifacts directory.
var directoryPath string
func init() {
if len(os.Args) > 1 {
directoryPath = os.Args[1]
} else {
currentDir, _ := os.Getwd()
directoryPath = filepath.Join(currentDir, "forge-artifacts")
}
}
// skipped returns true if the contract should be skipped when inspecting its storage layout.
func skipped(contractName string) bool {
return strings.Contains(contractName, "CrossDomainMessengerLegacySpacer")
}
// variableInfo represents the parsed variable information.
type variableInfo struct {
name string
slot int
offset int
length int
}
// parseVariableInfo parses out variable info from the variable structure in standard compiler json output.
func parseVariableInfo(variable map[string]interface{}) (variableInfo, error) {
var info variableInfo
var err error
info.name = variable["label"].(string)
info.slot, err = strconv.Atoi(variable["slot"].(string))
if err != nil {
return info, err
}
info.offset = int(variable["offset"].(float64))
variableType := variable["type"].(string)
if strings.HasPrefix(variableType, "t_mapping") {
info.length = 32
} else if strings.HasPrefix(variableType, "t_uint") {
re := regexp.MustCompile(`uint(\d+)`)
matches := re.FindStringSubmatch(variableType)
if len(matches) > 1 {
bitSize, _ := strconv.Atoi(matches[1])
info.length = bitSize / 8
}
} else if strings.HasPrefix(variableType, "t_bytes_") {
info.length = 32
} else if strings.HasPrefix(variableType, "t_bytes") {
re := regexp.MustCompile(`bytes(\d+)`)
matches := re.FindStringSubmatch(variableType)
if len(matches) > 1 {
info.length, _ = strconv.Atoi(matches[1])
}
} else if strings.HasPrefix(variableType, "t_address") {
info.length = 20
} else if strings.HasPrefix(variableType, "t_bool") {
info.length = 1
} else if strings.HasPrefix(variableType, "t_array") {
re := regexp.MustCompile(`^t_array\((\w+)\)(\d+)`)
matches := re.FindStringSubmatch(variableType)
if len(matches) > 2 {
innerType := matches[1]
size, _ := strconv.Atoi(matches[2])
innerInfo, err := parseVariableInfo(map[string]interface{}{
"label": variable["label"],
"offset": variable["offset"],
"slot": variable["slot"],
"type": innerType,
})
if err != nil {
return info, err
}
info.length = innerInfo.length * size
}
} else {
return info, fmt.Errorf("%s: unsupported type %s, add it to the script", info.name, variableType)
}
return info, nil
}
func main() {
err := filepath.Walk(directoryPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || strings.Contains(path, "t.sol") {
return nil
}
raw, err := os.ReadFile(path)
if err != nil {
return err
}
var artifact map[string]interface{}
err = json.Unmarshal(raw, &artifact)
if err != nil {
return err
}
storageLayout, ok := artifact["storageLayout"].(map[string]interface{})
if !ok {
return nil
}
storage, ok := storageLayout["storage"].([]interface{})
if !ok {
return nil
}
for _, v := range storage {
variable := v.(map[string]interface{})
fqn := variable["contract"].(string)
if skipped(fqn) {
continue
}
label := variable["label"].(string)
if strings.HasPrefix(label, "spacer_") {
parts := strings.Split(label, "_")
if len(parts) != 4 {
return fmt.Errorf("invalid spacer name format: %s", label)
}
slot, _ := strconv.Atoi(parts[1])
offset, _ := strconv.Atoi(parts[2])
length, _ := strconv.Atoi(parts[3])
variableInfo, err := parseVariableInfo(variable)
if err != nil {
return err
}
if slot != variableInfo.slot {
return fmt.Errorf("%s %s is in slot %d but should be in %d", fqn, label, variableInfo.slot, slot)
}
if offset != variableInfo.offset {
return fmt.Errorf("%s %s is at offset %d but should be at %d", fqn, label, variableInfo.offset, offset)
}
if length != variableInfo.length {
return fmt.Errorf("%s %s is %d bytes long but should be %d", fqn, label, variableInfo.length, length)
}
fmt.Printf("%s.%s is valid\n", fqn, label)
}
}
return nil
})
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}
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