Skip to content

Commit

Permalink
feat: solidity formatter MVP (#201)
Browse files Browse the repository at this point in the history
* feat: add fmt pallet

* feat: add formatter skeleton

* feat(fmt): Visitable for source & contract parts

* dyn -> impl, contract & enum impls, tests

* add empty_brackets helper

* use indent_write, implement more visitors

* cleanup test_formatter helper

* better brackets helpers

* add multiline support and respect to line_length

* fix clippy

* remove itertools dep

* add llvm to ci tests

* add llvm to env var

* add llvm install and env var set to lint step

* respect basic style guide

* fix Cargo.lock

* use solang without LLVM

* update solang, remove LLVM from CI

* bump solang

* final solang bump

* add README.md, improve comments

* fix README.md

* test against prettier-plugin-solidity snapshots

* add submodules recursively in CI

* Revert "chore: remove integration tests (#196)"

This reverts commit bab7e2d.

* add fmt testdata to Makefile

* run make fmt-testdata in CI

* cli & better formatting

* fix constructor function corner case

* use separate solang-parser crate

* add --check option

* bump solang, fix missing parts regarding source visiting

* bump solang, refactor blank lines formatting

* fix semver parsing (remove it lol)

* improve cli args

* parallel formatting w/ rayon, improve errors

* fix Makefile fmt-testdata dir structure

* fixes after matt's review

* good looking diffs on --check

* fix lint

* more default Visitor implementations

* bump solang, improve doc comments

* fix clippy

Signed-off-by: Alexey Shekhirin <[email protected]>

* bump solang

Signed-off-by: Alexey Shekhirin <[email protected]>

* comment out fmt from cli

* support error definitions & fix clippy

Co-authored-by: Matthias Seitz <[email protected]>
  • Loading branch information
shekhirin and mattsse authored Feb 14, 2022
1 parent 1582882 commit ff4ec0f
Show file tree
Hide file tree
Showing 16 changed files with 1,219 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
- name: Checkout sources
uses: actions/checkout@v2

- name: Clone testdata for fmt tests
run: make fmt-testdata

- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/target
out/
out.json

fmt/testdata/*
29 changes: 29 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"cli",
"cli/test-utils",
"config",
"fmt",
]

# Binary size optimizations
Expand All @@ -24,4 +25,4 @@ debug = true
#ethers-providers = { path = "../ethers-rs/ethers-providers" }
#ethers-signers = { path = "../ethers-rs/ethers-signers" }
#ethers-etherscan = { path = "../ethers-rs/ethers-etherscan" }
#ethers-solc = { path = "../ethers-rs/ethers-solc" }
#ethers-solc = { path = "../ethers-rs/ethers-solc" }
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.PHONY: fmt-testdata

fmt-testdata:
@FOLDER=$(shell dirname "$0")/fmt/testdata/prettier-plugin-solidity;\
if [ ! -d $$FOLDER/.git ] ; then git clone --depth 1 --recursive https://github.com/prettier-solidity/prettier-plugin-solidity $$FOLDER;\
else cd $$FOLDER; git pull --recurse-submodules; fi
7 changes: 6 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ clap = { version = "3.0.10", features = [
"wrap_help",
] }
clap_complete = "3.0.4"
forge-fmt = { path = "../fmt" }
foundry-utils = { path = "../utils" }
forge = { path = "../forge" }
foundry-config = { path = "../config" }
Expand All @@ -41,7 +42,7 @@ rpassword = "5.0.1"
tracing-subscriber = "0.2.20"
tracing = "0.1.26"
hex = "0.4.3"
rayon = "1.5"
rayon = "1.5.1"
serde = "1.0.133"

## EVM Implementations
Expand All @@ -52,6 +53,10 @@ glob = "0.3.0"
semver = "1.0.4"
once_cell = "1.9.0"
locate-cargo-manifest = "0.2.2"
walkdir = "2.3.2"
solang-parser = "0.1.2"
similar = { version = "2.1.0", features = ["inline"] }
console = "0.15.0"

[dev-dependencies]
foundry-utils = { path = "./../utils", features = ["test"] }
Expand Down
150 changes: 150 additions & 0 deletions cli/src/cmd/fmt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use std::{fmt::Write, path::PathBuf};

use clap::Parser;
use console::{style, Style};
use ethers::solc::ProjectPathsConfig;
use rayon::prelude::*;
use similar::{ChangeTag, TextDiff};

use forge_fmt::{Formatter, FormatterConfig, Visitable};

use crate::cmd::Cmd;

#[derive(Debug, Clone, Parser)]
pub struct FmtArgs {
#[clap(help = "path to the file or directory", conflicts_with = "root")]
path: Option<PathBuf>,
#[clap(help = "project's root path, default being the current working directory", long)]
root: Option<PathBuf>,
#[clap(
help = "run in 'check' mode. Exits with 0 if input is formatted correctly. Exits with 1 if formatting is required.",
long
)]
check: bool,
}

struct Line(Option<usize>);

impl std::fmt::Display for Line {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.0 {
None => write!(f, " "),
Some(idx) => write!(f, "{:<4}", idx + 1),
}
}
}

impl Cmd for FmtArgs {
type Output = ();

fn run(self) -> eyre::Result<Self::Output> {
let root = if let Some(path) = self.path {
path
} else {
let root = self.root.unwrap_or_else(|| {
std::env::current_dir().expect("failed to get current directory")
});
if !root.is_dir() {
return Err(eyre::eyre!("Root path should be a directory"))
}

ProjectPathsConfig::find_source_dir(&root)
};

let paths = if root.is_dir() {
ethers::solc::utils::source_files(root)
} else if root.file_name().unwrap().to_string_lossy().ends_with(".sol") {
vec![root]
} else {
vec![]
};

let diffs = paths
.par_iter()
.enumerate()
.map(|(i, path)| {
let source = std::fs::read_to_string(&path)?;
let (mut source_unit, _comments) = solang_parser::parse(&source, i)
.map_err(|diags| eyre::eyre!(
"Failed to parse Solidity code for {}. Leaving source unchanged.\nDebug info: {:?}",
path.to_string_lossy(),
diags
))?;

let mut output = String::new();
let mut formatter =
Formatter::new(&mut output, &source, FormatterConfig::default());

source_unit.visit(&mut formatter).unwrap();

solang_parser::parse(&output, 0).map_err(|diags| {
eyre::eyre!(
"Failed to construct valid Solidity code for {}. Leaving source unchanged.\nDebug info: {:?}",
path.to_string_lossy(),
diags
)
})?;

if self.check {
let diff = TextDiff::from_lines(&source, &output);

if diff.ratio() < 1.0 {
let mut diff_summary = String::new();

writeln!(diff_summary, "Diff in {}:", path.to_string_lossy())?;
for (j, group) in diff.grouped_ops(3).iter().enumerate() {
if j > 0 {
writeln!(diff_summary, "{:-^1$}", "-", 80)?;
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, s) = match change.tag() {
ChangeTag::Delete => ("-", Style::new().red()),
ChangeTag::Insert => ("+", Style::new().green()),
ChangeTag::Equal => (" ", Style::new().dim()),
};
write!(
diff_summary,
"{}{} |{}",
style(Line(change.old_index())).dim(),
style(Line(change.new_index())).dim(),
s.apply_to(sign).bold(),
)?;
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
write!(diff_summary, "{}", s.apply_to(value).underlined().on_black())?;
} else {
write!(diff_summary, "{}", s.apply_to(value))?;
}
}
if change.missing_newline() {
writeln!(diff_summary)?;
}
}
}
}

return Ok(Some(diff_summary))
}
} else {
std::fs::write(path, output)?;
}

Ok(None)
})
.collect::<eyre::Result<Vec<Option<String>>>>()?;

if !diffs.is_empty() {
for (i, diff) in diffs.iter().flatten().enumerate() {
if i > 0 {
println!();
}
print!("{}", diff);
}

std::process::exit(1);
}

Ok(())
}
}
1 change: 1 addition & 0 deletions cli/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod build;
pub mod config;
pub mod create;
pub mod flatten;
pub mod fmt;
pub mod init;
pub mod install;
pub mod remappings;
Expand Down
3 changes: 3 additions & 0 deletions cli/src/forge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ fn main() -> eyre::Result<()> {
Subcommands::Snapshot(cmd) => {
cmd.run()?;
}
// Subcommands::Fmt(cmd) => {
// cmd.run()?;
// }
Subcommands::Config(cmd) => {
cmd.run()?;
}
Expand Down
2 changes: 2 additions & 0 deletions cli/src/opts/forge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ pub enum Subcommands {

#[clap(about = "Concats a file with all of its imports")]
Flatten(flatten::FlattenArgs),
// #[clap(about = "formats Solidity source files")]
// Fmt(FmtArgs),
}

// A set of solc compiler settings that can be set via command line arguments, which are intended
Expand Down
13 changes: 13 additions & 0 deletions fmt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "forge-fmt"
version = "0.1.0"
edition = "2021"
description = "Foundry's solidity formatting and linting support"

[dependencies]
indent_write = "2.2.0"
semver = "1.0.4"
solang-parser = "0.1.2"

[dev-dependencies]
pretty_assertions = "1.0.0"
Loading

0 comments on commit ff4ec0f

Please sign in to comment.