Skip to content

Commit

Permalink
Integrate bindings assertions and add a custom command to check them
Browse files Browse the repository at this point in the history
  • Loading branch information
ggiraldez committed Jun 17, 2024
1 parent bc65581 commit e832e1e
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 9 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<(
.map(|_| ())
}

pub(crate) fn parse_source_file<F>(
pub fn parse_source_file<F>(
file_path_string: &str,
version: Version,
run_before_checking: F,
Expand Down
6 changes: 4 additions & 2 deletions crates/solidity/outputs/cargo/slang_solidity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand All @@ -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 }
Expand Down
205 changes: 205 additions & 0 deletions crates/solidity/outputs/cargo/slang_solidity/src/assertions.rs
Original file line number Diff line number Diff line change
@@ -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<Assertions, AssertionError> {
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<String, Assertion>,
references: Vec<Assertion>,
}

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<Item = &Assertion> {
self.definitions.values().chain(self.references.iter())
}
}

#[derive(Clone, Debug, PartialEq)]
enum Assertion {
Definition { id: String, cursor: Cursor },
Reference { id: Option<String>, 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<Option<Assertion>, 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<Cursor> {
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
}
40 changes: 40 additions & 0 deletions crates/solidity/outputs/cargo/slang_solidity/src/commands.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/solidity/outputs/cargo/slang_solidity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _};
}
Loading

0 comments on commit e832e1e

Please sign in to comment.