diff --git a/.github/workflows/sanctuary.yml b/.github/workflows/sanctuary.yml index a3298ecc23..c79e2a3aea 100644 --- a/.github/workflows/sanctuary.yml +++ b/.github/workflows/sanctuary.yml @@ -15,6 +15,11 @@ on: type: "string" required: true default: "mainnet" + check_bindings: + description: "Check name bindings on contracts, failing if there's any unresolved symbol." + type: "boolean" + required: false + default: false jobs: sanctuary: @@ -54,4 +59,4 @@ jobs: - name: "infra run solidity_testing_sanctuary" uses: "./.github/actions/devcontainer/run" with: - runCmd: "./scripts/bin/infra run --release --bin solidity_testing_sanctuary -- --shards-count ${{ env.SHARDS_COUNT }} --shard-index ${{ matrix.shard_index }} ${{ inputs.chain }} ${{ inputs.network }}" + runCmd: "./scripts/bin/infra run --release --bin solidity_testing_sanctuary -- --shards-count ${{ env.SHARDS_COUNT }} --shard-index ${{ matrix.shard_index }} ${{ inputs.check_bindings == true && '--check-bindings' || '' }} ${{ inputs.chain }} ${{ inputs.network }}" diff --git a/Cargo.lock b/Cargo.lock index 4f95de0ed8..0256970109 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2367,6 +2367,7 @@ dependencies = [ "indicatif", "infra_utils", "itertools 0.13.0", + "metaslang_bindings", "once_cell", "rayon", "semver", diff --git a/crates/solidity/inputs/language/bindings/rules.msgb b/crates/solidity/inputs/language/bindings/rules.msgb index 42a3bd16e9..db1390c6cc 100644 --- a/crates/solidity/inputs/language/bindings/rules.msgb +++ b/crates/solidity/inputs/language/bindings/rules.msgb @@ -2265,9 +2265,11 @@ inherit .lexical_scope edge @path.lexical_scope -> @expr.lexical_scope } -@path [YulPath @name [YulIdentifier]] { +@path [YulPath] { node @path.lexical_scope +} +@path [YulPath @name [YulIdentifier]] { node ref attr (ref) node_reference = @name diff --git a/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs b/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs index e60d727395..c28dc2449a 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs @@ -2270,9 +2270,11 @@ inherit .lexical_scope edge @path.lexical_scope -> @expr.lexical_scope } -@path [YulPath @name [YulIdentifier]] { +@path [YulPath] { node @path.lexical_scope +} +@path [YulPath @name [YulIdentifier]] { node ref attr (ref) node_reference = @name diff --git a/crates/solidity/testing/sanctuary/Cargo.toml b/crates/solidity/testing/sanctuary/Cargo.toml index a65bec5bef..af8812c946 100644 --- a/crates/solidity/testing/sanctuary/Cargo.toml +++ b/crates/solidity/testing/sanctuary/Cargo.toml @@ -17,7 +17,8 @@ rayon = { workspace = true } semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -slang_solidity = { workspace = true, features = ["__private_ariadne_errors"] } +metaslang_bindings = { workspace = true } +slang_solidity = { workspace = true, features = ["__private_ariadne_errors", "__experimental_bindings_api"] } strum_macros = { workspace = true } url = { workspace = true } diff --git a/crates/solidity/testing/sanctuary/src/events.rs b/crates/solidity/testing/sanctuary/src/events.rs index 93d67b3634..d74bb8445c 100644 --- a/crates/solidity/testing/sanctuary/src/events.rs +++ b/crates/solidity/testing/sanctuary/src/events.rs @@ -106,7 +106,7 @@ impl Events { }; } - pub fn parse_error(&self, message: impl AsRef) { + fn test_error(&self, message: impl AsRef) { match self.failed.position().cmp(&MAX_PRINTED_FAILURES) { cmp::Ordering::Less => { self.reporter.println(message); @@ -122,6 +122,14 @@ impl Events { }; } + pub fn parse_error(&self, message: impl AsRef) { + self.test_error(message); + } + + pub fn bindings_error(&self, message: impl AsRef) { + self.test_error(message); + } + pub fn trace(&self, message: impl AsRef) { self.reporter.println(message); } diff --git a/crates/solidity/testing/sanctuary/src/main.rs b/crates/solidity/testing/sanctuary/src/main.rs index 8ad033c622..80f39766d6 100644 --- a/crates/solidity/testing/sanctuary/src/main.rs +++ b/crates/solidity/testing/sanctuary/src/main.rs @@ -27,6 +27,10 @@ struct Cli { /// Disables parallelism, and logs traces to help with debugging errors or panics. #[arg(long, default_value_t = false)] trace: bool, + + /// Enables checking bindings for each contract, failing if any symbol cannot be resolved. + #[arg(long, default_value_t = false)] + check_bindings: bool, } #[derive(Debug, Parser)] @@ -45,6 +49,7 @@ fn main() -> Result<()> { chain, sharding_options, trace, + check_bindings, } = Cli::parse(); Terminal::step(format!( @@ -80,9 +85,9 @@ fn main() -> Result<()> { events.start_directory(files.len()); if trace { - run_with_traces(files, &events)?; + run_with_traces(files, &events, check_bindings)?; } else { - run_in_parallel(files, &events)?; + run_in_parallel(files, &events, check_bindings)?; } events.finish_directory(); @@ -104,14 +109,14 @@ fn main() -> Result<()> { Ok(()) } -fn run_with_traces(files: &Vec, events: &Events) -> Result<()> { +fn run_with_traces(files: &Vec, events: &Events, check_bindings: bool) -> Result<()> { for file in files { let compiler = &file.compiler; let path = file.path.strip_repo_root()?; events.trace(format!("[{compiler}] Starting: {path:?}")); - run_test(file, events)?; + run_test(file, events, check_bindings)?; events.trace(format!("[{compiler}] Finished: {path:?}")); } @@ -119,11 +124,11 @@ fn run_with_traces(files: &Vec, events: &Events) -> Result<()> { Ok(()) } -fn run_in_parallel(files: &Vec, events: &Events) -> Result<()> { +fn run_in_parallel(files: &Vec, events: &Events, check_bindings: bool) -> Result<()> { files .par_iter() .panic_fuse(/* Halt as soon as possible if a child panics */) - .try_for_each(|file| run_test(file, events)) + .try_for_each(|file| run_test(file, events, check_bindings)) } #[test] diff --git a/crates/solidity/testing/sanctuary/src/tests.rs b/crates/solidity/testing/sanctuary/src/tests.rs index 054ef801be..6571da0b11 100644 --- a/crates/solidity/testing/sanctuary/src/tests.rs +++ b/crates/solidity/testing/sanctuary/src/tests.rs @@ -1,12 +1,17 @@ use std::cmp::min; use std::path::Path; +use std::sync::Arc; use anyhow::Result; use infra_utils::paths::PathExtensions; use itertools::Itertools; +use metaslang_bindings::PathResolver; use semver::Version; -use slang_solidity::cst::NonterminalKind; -use slang_solidity::parser::Parser; +use slang_solidity::bindings::Bindings; +use slang_solidity::cst::{Cursor, NonterminalKind, TextIndex, TextRange}; +use slang_solidity::diagnostic::{Diagnostic, Severity}; +use slang_solidity::parser::{ParseOutput, Parser}; +use slang_solidity::{bindings, transform_built_ins_node}; use crate::datasets::{DataSet, SourceFile}; use crate::events::{Events, TestOutcome}; @@ -59,7 +64,7 @@ pub(crate) fn select_tests<'d>( } } -pub fn run_test(file: &SourceFile, events: &Events) -> Result<()> { +pub fn run_test(file: &SourceFile, events: &Events, check_bindings: bool) -> Result<()> { if !file.path.exists() { // Index can be out of date: events.test(TestOutcome::NotFound); @@ -100,23 +105,36 @@ pub fn run_test(file: &SourceFile, events: &Events) -> Result<()> { let parser = Parser::create(version.clone())?; let output = parser.parse(NonterminalKind::SourceUnit, &source); + let source_id = file.path.strip_repo_root()?.unwrap_str(); - if output.is_valid() { - events.test(TestOutcome::Passed); - return Ok(()); - } + let with_color = true; - events.test(TestOutcome::Failed); + if !output.is_valid() { + for error in output.errors() { + let report = slang_solidity::diagnostic::render(error, source_id, &source, with_color); - let with_color = true; - let source_id = file.path.strip_repo_root()?.unwrap_str(); + events.parse_error(format!("[{version}] {report}")); + } - for error in output.errors() { - let report = slang_solidity::diagnostic::render(error, source_id, &source, with_color); + events.test(TestOutcome::Failed); + return Ok(()); + } - events.parse_error(format!("[{version}] {report}")); + if check_bindings { + let unresolved_references = run_bindings_check(&version, source_id, &output)?; + if !unresolved_references.is_empty() { + for unresolved in &unresolved_references { + let report = + slang_solidity::diagnostic::render(unresolved, source_id, &source, with_color); + events.bindings_error(format!("[{version}] {report}")); + } + + events.test(TestOutcome::Failed); + return Ok(()); + } } + events.test(TestOutcome::Passed); Ok(()) } @@ -161,3 +179,83 @@ fn uses_exotic_parser_bug(file: &Path) -> bool { .iter() .any(|path| file.ends_with(path)) } + +fn run_bindings_check( + version: &Version, + source_id: &str, + output: &ParseOutput, +) -> Result> { + let mut unresolved = Vec::new(); + let bindings = create_bindings(version, source_id, output)?; + + for reference in bindings.all_references() { + if reference.get_file().is_system() { + // skip built-ins + continue; + } + // We're not interested in the exact definition a reference resolves + // to, so we lookup all of them and fail if we find none. + if reference.definitions().is_empty() { + let cursor = reference.get_cursor().unwrap(); + unresolved.push(UnresolvedReference { cursor }); + } + } + + Ok(unresolved) +} + +fn create_bindings(version: &Version, source_id: &str, output: &ParseOutput) -> Result { + let mut bindings = bindings::create_with_resolver( + version.clone(), + Arc::new(SingleFileResolver { + source_id: source_id.into(), + }), + ); + let parser = Parser::create(version.clone())?; + let built_ins_tree = parser + .parse( + NonterminalKind::SourceUnit, + bindings::get_built_ins(version), + ) + .tree(); + let built_ins_cursor = + transform_built_ins_node(&built_ins_tree).cursor_with_offset(TextIndex::ZERO); + + bindings.add_system_file("built_ins.sol", built_ins_cursor); + bindings.add_user_file(source_id, output.create_tree_cursor()); + Ok(bindings) +} + +/// Bindings `PathResolver` that always resolves to the given `source_id`. +/// This is useful for Sanctuary since all dependencies are concatenated in the +/// same file, but the import directives are retained. +struct SingleFileResolver { + source_id: String, +} + +impl PathResolver for SingleFileResolver { + fn resolve_path(&self, _context_path: &str, _path_to_resolve: &str) -> Option { + Some(self.source_id.clone()) + } +} + +struct UnresolvedReference { + pub cursor: Cursor, +} + +impl Diagnostic for UnresolvedReference { + fn text_range(&self) -> TextRange { + self.cursor.text_range() + } + + fn severity(&self) -> Severity { + Severity::Error + } + + fn message(&self) -> String { + format!( + "Unresolved reference to `{symbol}`", + symbol = self.cursor.node().unparse() + ) + } +}