From e832e1ecb274b3bc478c5c0a45047a9dc8467fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Gir=C3=A1ldez?= Date: Mon, 17 Jun 2024 13:09:43 -0400 Subject: [PATCH] Integrate bindings assertions and add a custom command to check them --- Cargo.lock | 1 + .../cargo/src/runtime/cli/commands/mod.rs | 3 + .../cargo/src/runtime/cli/commands/parse.rs | 2 +- .../outputs/cargo/slang_solidity/Cargo.toml | 6 +- .../cargo/slang_solidity/src/assertions.rs | 205 ++++++++++++++++++ .../cargo/slang_solidity/src/commands.rs | 40 ++++ .../src/generated/cli/commands/mod.rs | 3 + .../src/generated/cli/commands/parse.rs | 2 +- .../outputs/cargo/slang_solidity/src/lib.rs | 4 +- .../outputs/cargo/slang_solidity/src/main.rs | 68 +++++- .../src/generated/cli/commands/mod.rs | 3 + .../src/generated/cli/commands/parse.rs | 2 +- 12 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/assertions.rs create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/commands.rs diff --git a/Cargo.lock b/Cargo.lock index e25a9eb30..e6f8bb467 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2079,6 +2079,7 @@ dependencies = [ "infra_utils", "metaslang_cst", "metaslang_graph_builder", + "regex", "semver", "serde", "serde_json", diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs b/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs index 7af1614b2..3ef295957 100644 --- a/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs @@ -31,4 +31,7 @@ pub enum CommandError { #[cfg(feature = "__experimental_bindings_api")] #[error(transparent)] BindingsError(#[from] crate::bindings::BindingsError), + + #[error("Unknown error: {0}")] + Unknown(String), } diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs b/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs index 496d3dbe5..6e149fbdf 100644 --- a/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs @@ -19,7 +19,7 @@ pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<( .map(|_| ()) } -pub(crate) fn parse_source_file( +pub fn parse_source_file( file_path_string: &str, version: Version, run_before_checking: F, diff --git a/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml b/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml index ccc001811..84f8661a0 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml +++ b/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml @@ -33,11 +33,11 @@ required-features = ["cli"] [features] default = ["cli"] -cli = ["dep:clap", "dep:serde_json", "__private_ariadne"] +cli = ["dep:anyhow", "dep:clap", "dep:serde_json", "__private_ariadne"] # This is meant to be used by the CLI or internally only. __private_ariadne = ["dep:ariadne"] # For internal development only -__experimental_bindings_api = ["dep:metaslang_graph_builder", "dep:stack-graphs"] +__experimental_bindings_api = ["dep:metaslang_graph_builder", "dep:stack-graphs", "dep:regex"] [build-dependencies] # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ anyhow = { workspace = true } # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ @@ -46,10 +46,12 @@ infra_utils = { workspace = true } # __REMOVE_THIS_LINE_DURING_CAR solidity_language = { workspace = true } # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ [dependencies] +anyhow = { workspace = true, optional = true } ariadne = { workspace = true, optional = true } clap = { workspace = true, optional = true } metaslang_cst = { workspace = true } metaslang_graph_builder = { workspace = true, optional = true } +regex = { workspace = true, optional = true } semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, optional = true } diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/assertions.rs b/crates/solidity/outputs/cargo/slang_solidity/src/assertions.rs new file mode 100644 index 000000000..73beb6a02 --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/assertions.rs @@ -0,0 +1,205 @@ +use core::fmt; +use std::cmp::Ordering; +use std::collections::HashMap; + +use regex::Regex; +use semver::Version; +use thiserror::Error; +use anyhow::Result; + +use slang_solidity::bindings::Bindings; +use slang_solidity::cursor::Cursor; +use slang_solidity::kinds::TerminalKind; +use slang_solidity::query::Query; + +use slang_solidity::cli::commands::CommandError; +use slang_solidity::cli::commands; + +pub fn execute_check_assertions(file_path_string: &str, version: Version) -> Result<()> { + let mut bindings = Bindings::create(version.clone()); + let parse_output = commands::parse::parse_source_file(file_path_string, version, |_| ())?; + let tree_cursor = parse_output.create_tree_cursor(); + + bindings.add_file(file_path_string, tree_cursor.clone())?; + + let assertions = + collect_assertions(tree_cursor).map_err(|e| CommandError::Unknown(e.to_string()))?; + for assertion in assertions.iter() { + println!("{}", assertion); + } + Ok(()) +} + +#[derive(Debug, Error)] +enum AssertionError { + #[error("Invalid assertion at {0}:{1}")] + InvalidAssertion(usize, usize), + + #[error("Duplicate assertion definition {0}")] + DuplicateDefinition(String), +} + +fn collect_assertions(cursor: Cursor) -> Result { + let mut assertions = Assertions::new(); + + let query = Query::parse("@comment [SingleLineComment]").unwrap(); + for result in cursor.query(vec![query]) { + let captures = result.captures; + let Some(comment) = captures.get("comment").and_then(|v| v.first()) else { + continue; + }; + + if let Some(assertion) = find_assertion_in_comment(comment)? { + assertions.insert_assertion(assertion)?; + } + } + + Ok(assertions) +} + +struct Assertions { + definitions: HashMap, + references: Vec, +} + +impl Assertions { + fn new() -> Self { + Self { + definitions: HashMap::new(), + references: Vec::new(), + } + } + + fn insert_assertion(&mut self, assertion: Assertion) -> Result<(), AssertionError> { + match assertion { + Assertion::Definition { ref id, .. } => { + if self.definitions.contains_key(id) { + Err(AssertionError::DuplicateDefinition(id.clone())) + } else { + self.definitions.insert(id.clone(), assertion); + Ok(()) + } + } + Assertion::Reference { .. } => { + self.references.push(assertion); + Ok(()) + } + } + } + + fn iter(&self) -> impl Iterator { + self.definitions.values().chain(self.references.iter()) + } +} + +#[derive(Clone, Debug, PartialEq)] +enum Assertion { + Definition { id: String, cursor: Cursor }, + Reference { id: Option, cursor: Cursor }, +} + +impl fmt::Display for Assertion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Assert ")?; + let cursor = match self { + Self::Definition { id, cursor } => { + write!(f, "Definition {}", id,)?; + cursor + } + Self::Reference { id: None, cursor } => { + write!(f, "Unresolved Reference",)?; + cursor + } + Self::Reference { + id: Some(id), + cursor, + } => { + write!(f, "Reference {}", id,)?; + cursor + } + }; + let offset = cursor.text_offset(); + let range = cursor.text_range(); + write!( + f, + " `{}` at {}:{} [{}..{}]", + cursor.node().unparse(), + offset.line + 1, + offset.column + 1, + range.start, + range.end, + ) + } +} + +fn find_assertion_in_comment(comment: &Cursor) -> Result, AssertionError> { + let assertion_regex = Regex::new(r"[\^](ref|def):([0-9a-zA-Z_-]+|!)").unwrap(); + let comment_offset = comment.text_offset(); + let comment_col = comment_offset.column; + let comment_str = comment.node().unparse(); + + let Some(captures) = assertion_regex.captures(&comment_str) else { + return Ok(None); + }; + + let assertion_id = captures.get(2).unwrap().as_str(); + let assertion_type = captures.get(1).unwrap().as_str(); + let assertion_col = comment_col + captures.get(0).unwrap().start(); + + if let Some(cursor) = search_asserted_node_backwards(comment.clone(), assertion_col) { + let assertion = match assertion_type { + "ref" => { + let id = if assertion_id == "!" { + // this should be an unresolved reference + None + } else { + Some(assertion_id.to_owned()) + }; + Assertion::Reference { id, cursor } + } + "def" => Assertion::Definition { + id: assertion_id.to_owned(), + cursor, + }, + _ => unreachable!("unknown assertion type"), + }; + Ok(Some(assertion)) + } else { + Err(AssertionError::InvalidAssertion( + comment_offset.line + 1, + assertion_col + 1, + )) + } +} + +fn search_asserted_node_backwards(mut cursor: Cursor, anchor_column: usize) -> Option { + let starting_line = cursor.text_offset().line; + while cursor.go_to_previous() { + // Skip if the cursor is on the same line + if cursor.text_offset().line == starting_line { + continue; + } + + // Skip over trivia and other comments (allows defining multiple + // assertions for the same line of code in multiple single line + // comments) + if cursor.node().is_terminal_with_kinds(&[ + TerminalKind::Whitespace, + TerminalKind::EndOfLine, + TerminalKind::SingleLineComment, + ]) { + continue; + } + + let cursor_column = cursor.text_offset().column; + match cursor_column.cmp(&anchor_column) { + Ordering::Equal => return Some(cursor), + Ordering::Greater => continue, + _ => (), + } + + // Node is not found, and probably the anchor is invalid + break; + } + None +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/commands.rs b/crates/solidity/outputs/cargo/slang_solidity/src/commands.rs new file mode 100644 index 000000000..32d878fb6 --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/commands.rs @@ -0,0 +1,40 @@ +#[allow(unused_imports)] +use anyhow::Error; +use clap::Subcommand; +use std::process::ExitCode; + +#[derive(Subcommand, Debug)] +pub enum CustomCommands { + #[cfg(feature = "__experimental_bindings_api")] + CheckAssertions { + /// File path to the source file to parse + file_path: String, + + /// The language version to use for parsing + #[arg(short, long)] + version: semver::Version, + }, +} + +impl CustomCommands { + #[cfg(not(feature = "__experimental_bindings_api"))] + pub fn execute(self) -> ExitCode { + unreachable!() + } + + #[cfg(feature = "__experimental_bindings_api")] + pub fn execute(self) -> ExitCode { + let result: Result<(), Error> = match self { + Self::CheckAssertions { file_path, version } => { + super::assertions::execute_check_assertions(&file_path, version) + } + }; + match result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } + } +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs index 3ad74bc6a..a441ed946 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs @@ -33,4 +33,7 @@ pub enum CommandError { #[cfg(feature = "__experimental_bindings_api")] #[error(transparent)] BindingsError(#[from] crate::bindings::BindingsError), + + #[error("Unknown error: {0}")] + Unknown(String), } diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs index c2ed11a90..decba0fc0 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs @@ -21,7 +21,7 @@ pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<( .map(|_| ()) } -pub(crate) fn parse_source_file( +pub fn parse_source_file( file_path_string: &str, version: Version, run_before_checking: F, diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs b/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs index ff73e70f3..9aee541d9 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs @@ -8,5 +8,7 @@ pub use generated::*; // https://github.com/rust-lang/cargo/issues/1982 #[cfg(feature = "cli")] mod supress_cli_dependencies { - use {ariadne as _, clap as _, serde_json as _}; + #[cfg(feature = "__experimental_bindings_api")] + use regex as _; + use {anyhow as _, ariadne as _, clap as _, serde_json as _}; } diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/main.rs b/crates/solidity/outputs/cargo/slang_solidity/src/main.rs index 72c0f6028..f99c759a5 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/main.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/main.rs @@ -1,7 +1,10 @@ use std::process::ExitCode; -use clap::Parser as ClapParser; -use slang_solidity::cli::Commands; +use clap::{ + error::Error, error::ErrorKind, ArgMatches, Command, FromArgMatches, Parser as ClapParser, + Subcommand, +}; +use slang_solidity::cli; // Below are dependencies used by the API `lib.rs`, but not the CLI "main.rs". // However, we need to add a fake usage to suppress Cargo warnings about unused dependencies. @@ -16,12 +19,71 @@ mod supress_api_dependencies { use {metaslang_graph_builder as _, stack_graphs as _}; } +#[cfg(feature = "__experimental_bindings_api")] +mod assertions; +mod commands; + +use commands::CustomCommands; + #[derive(ClapParser, Debug)] #[command(next_line_help = true)] #[command(author, about)] struct Cli { #[command(subcommand)] - command: Commands, + command: CliCommand, +} + +#[derive(Debug)] +enum CliCommand { + Common(cli::Commands), + Custom(CustomCommands), +} + +impl CliCommand { + fn execute(self) -> ExitCode { + match self { + Self::Common(command) => command.execute(), + Self::Custom(command) => command.execute(), + } + } +} + +impl FromArgMatches for CliCommand { + fn from_arg_matches(matches: &ArgMatches) -> Result { + if let Ok(common) = cli::Commands::from_arg_matches(matches) { + Ok(Self::Common(common)) + } else if let Ok(custom) = CustomCommands::from_arg_matches(matches) { + Ok(Self::Custom(custom)) + } else { + Err(Error::new(ErrorKind::MissingSubcommand)) + } + } + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> { + if let Ok(common) = cli::Commands::from_arg_matches(matches) { + *self = Self::Common(common); + } else if let Ok(custom) = CustomCommands::from_arg_matches(matches) { + *self = Self::Custom(custom); + } else { + return Err(Error::new(ErrorKind::MissingSubcommand)); + } + Ok(()) + } +} + +impl Subcommand for CliCommand { + fn augment_subcommands(cmd: Command) -> Command { + let cmd = cli::Commands::augment_subcommands(cmd); + let cmd = CustomCommands::augment_subcommands(cmd); + cmd + } + fn augment_subcommands_for_update(cmd: Command) -> Command { + let cmd = cli::Commands::augment_subcommands(cmd); + let cmd = CustomCommands::augment_subcommands(cmd); + cmd + } + fn has_subcommand(name: &str) -> bool { + cli::Commands::has_subcommand(name) || CustomCommands::has_subcommand(name) + } } fn main() -> ExitCode { diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs index 3ad74bc6a..a441ed946 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs @@ -33,4 +33,7 @@ pub enum CommandError { #[cfg(feature = "__experimental_bindings_api")] #[error(transparent)] BindingsError(#[from] crate::bindings::BindingsError), + + #[error("Unknown error: {0}")] + Unknown(String), } diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs index c2ed11a90..decba0fc0 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs @@ -21,7 +21,7 @@ pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<( .map(|_| ()) } -pub(crate) fn parse_source_file( +pub fn parse_source_file( file_path_string: &str, version: Version, run_before_checking: F,