forked from NomicFoundation/slang
-
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.
Integrate bindings assertions and add a custom command to check them
- Loading branch information
Showing
12 changed files
with
330 additions
and
9 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
205 changes: 205 additions & 0 deletions
205
crates/solidity/outputs/cargo/slang_solidity/src/assertions.rs
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,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
40
crates/solidity/outputs/cargo/slang_solidity/src/commands.rs
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,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 | ||
} | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
2 changes: 1 addition & 1 deletion
2
crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
Oops, something went wrong.