Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ Below is a simple example for an ERC-20 token, the full example repo can be foun

![erc20-scopelint-spec-example](./assets/spec.gif)

**Flags:**
- `--show-internal`: Include internal and private functions in the specification (by default, only public and external functions are shown)

Currently this feature is in beta, and we are looking for feedback on how to improve it.
Right now it's focused on specifications for unit tests, which are very useful for developers but less useful for higher-level stakeholders.
As a result, it does not yet include information about protocol invariants or integration test / user-story types of specifications.
Expand Down
6 changes: 5 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ pub enum Subcommands {
},
#[clap(about = "Generates a specification for the current project from test names.")]
/// Generates a specification for the current project from test names.
Spec,
Spec {
#[clap(long, help = "Show internal functions in the specification.")]
/// Show internal functions in the specification.
show_internal: bool,
},
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ pub fn run(opts: &config::Opts) -> Result<(), Box<dyn Error>> {
match &opts.subcommand {
config::Subcommands::Check => check::run(taplo_opts),
config::Subcommands::Fmt { check } => fmt::run(taplo_opts, *check),
config::Subcommands::Spec => spec::run(),
config::Subcommands::Spec { show_internal } => spec::run(*show_internal),
}
}
36 changes: 25 additions & 11 deletions src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ use walkdir::WalkDir;
/// Returns an error if the specification could not be generated from the Solidity code.
/// # Panics
/// Panics when a file path could not be unwrapped.
pub fn run() -> Result<(), Box<dyn Error>> {
pub fn run(show_internal: bool) -> Result<(), Box<dyn Error>> {
// =================================
// ======== Parse contracts ========
// =================================

// First, parse all source and test files to collect the contracts and their methods. All free
// functions are added under a special contract called `FreeFunctions`.
let src_contracts = get_contracts_for_dir("./src", ".sol");
let test_contracts = get_contracts_for_dir("./test", ".t.sol");
let src_contracts = get_contracts_for_dir("./src", ".sol", show_internal);
let test_contracts = get_contracts_for_dir("./test", ".t.sol", show_internal);

// ========================================
// ======== Generate Specification ========
Expand Down Expand Up @@ -72,8 +72,9 @@ struct ParsedContract {
}

impl ParsedContract {
fn new(path: PathBuf, contract: Option<ContractDefinition>) -> Self {
let functions = contract.as_ref().map_or(Vec::new(), get_functions_from_contract);
fn new(path: PathBuf, contract: Option<ContractDefinition>, show_internal: bool) -> Self {
let functions =
contract.as_ref().map_or(Vec::new(), |c| get_functions_from_contract(c, show_internal));
Self { path, contract, functions }
}

Expand Down Expand Up @@ -201,7 +202,11 @@ impl ProtocolSpecification {
// ======== Helper functions ========
// ==================================

fn get_contracts_for_dir<P: AsRef<Path>>(dir: P, extension: &str) -> Vec<ParsedContract> {
fn get_contracts_for_dir<P: AsRef<Path>>(
dir: P,
extension: &str,
show_internal: bool,
) -> Vec<ParsedContract> {
let mut contracts: Vec<ParsedContract> = Vec::new();
for result in WalkDir::new(dir) {
let dent = match result {
Expand All @@ -217,13 +222,13 @@ fn get_contracts_for_dir<P: AsRef<Path>>(dir: P, extension: &str) -> Vec<ParsedC
continue;
}

let new_contracts = parse_contracts(file);
let new_contracts = parse_contracts(file, show_internal);
contracts.extend(new_contracts);
}
contracts
}

fn parse_contracts(file: &Path) -> Vec<ParsedContract> {
fn parse_contracts(file: &Path, show_internal: bool) -> Vec<ParsedContract> {
let content = fs::read_to_string(file).unwrap();
let (pt, _comments) = solang_parser::parse(&content, 0).expect("Parsing failed");
let mut contracts: Vec<ParsedContract> = Vec::new();
Expand All @@ -239,19 +244,28 @@ fn parse_contracts(file: &Path) -> Vec<ParsedContract> {
continue;
}

contracts.push(ParsedContract::new(file.to_path_buf(), Some(*c.clone())));
contracts.push(ParsedContract::new(
file.to_path_buf(),
Some(*c.clone()),
show_internal,
));
}
_ => (),
}
}
contracts
}

fn get_functions_from_contract(contract: &ContractDefinition) -> Vec<FunctionDefinition> {
fn get_functions_from_contract(
contract: &ContractDefinition,
show_internal: bool,
) -> Vec<FunctionDefinition> {
let mut functions = Vec::new();
for element in &contract.parts {
if let ContractPart::FunctionDefinition(f) = element {
functions.push(*f.clone());
if show_internal || f.is_public_or_external() {
functions.push(*f.clone());
}
}
}
functions
Expand Down
38 changes: 37 additions & 1 deletion tests/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,48 @@ fn run_scopelint(test_folder: &str) -> Output {
.expect("Failed to execute command")
}

fn run_scopelint_with_flag(test_folder: &str, flag: &str) -> Output {
let cwd = env::current_dir().unwrap();
let project_path = cwd.join("tests").join(test_folder);
let binary_path = cwd.join("target/debug/scopelint");

Command::new(binary_path)
.current_dir(project_path)
.args(["spec", flag])
.output()
.expect("Failed to execute command")
}

#[test]
fn test_spec_proj1() {
fn test_spec_proj1_default() {
let output = run_scopelint("spec-proj1");
let stdout = String::from_utf8(output.stdout).unwrap();
let expected_spec = r#"
Contract Specification: ERC20
├── approve
│ ├── Sets Allowance Mapping To Approved Amount
│ ├── Returns True For Successful Approval
│ └── Emits Approval Event
├── transfer
│ ├── Revert If: Spender Has Insufficient Balance
│ ├── Does Not Change Total Supply
│ ├── Increases Recipient Balance By Sent Amount
│ ├── Decreases Sender Balance By Sent Amount
│ ├── Returns True
│ └── Emits Transfer Event
├── transferFrom
├── permit
└── DOMAIN_SEPARATOR
"#;
assert_eq!(stdout, expected_spec);
}

#[test]
fn test_spec_proj1_with_show_internal() {
let output = run_scopelint_with_flag("spec-proj1", "--show-internal");
let stdout = String::from_utf8(output.stdout).unwrap();
let expected_spec = r#"
Contract Specification: ERC20
├── constructor
│ ├── Stored Name Matches Constructor Input
│ ├── Stored Symbol Matches Constructor Input
Expand Down
Loading