Skip to content
Draft
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
7 changes: 7 additions & 0 deletions crates/forge/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ pub fn run_command(args: Forge) -> Result<()> {
global.block_on(cmd.run()).map(drop)
}
}
ForgeSubcommand::Check(cmd) => {
if cmd.is_watch() {
global.block_on(watch::watch_check(cmd))
} else {
global.block_on(cmd.run())
}
}
ForgeSubcommand::VerifyContract(args) => global.block_on(args.run()),
ForgeSubcommand::VerifyCheck(args) => global.block_on(args.run()),
ForgeSubcommand::VerifyBytecode(cmd) => global.block_on(cmd.run()),
Expand Down
181 changes: 181 additions & 0 deletions crates/forge/src/cmd/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use super::{install, watch::WatchArgs};
use clap::Parser;
use eyre::Result;
use foundry_cli::{
opts::{BuildOpts, configure_pcx_from_solc},
utils::LoadConfig,
};
use foundry_common::shell;
use foundry_compilers::{
CompilerInput, Graph, Language, Project,
artifacts::{Source, Sources},
compilers::multi::MultiCompilerLanguage,
multi::MultiCompilerParser,
solc::{SolcLanguage, SolcVersionedInput},
utils::source_files_iter,
};
use foundry_config::{
Config,
figment::{
self, Metadata, Profile, Provider,
error::Kind::InvalidType,
value::{Map, Value},
},
};
use serde::Serialize;
use solar::sema::Compiler;
use std::path::PathBuf;

foundry_config::merge_impl_figment_convert!(CheckArgs, build);

/// CLI arguments for `forge check`.
///
/// Similar to `forge build`, but only performs parsing, lowering and semantic analysis.
/// Skips codegen and optimizer steps for faster feedback.
#[derive(Clone, Debug, Default, Serialize, Parser)]
#[command(next_help_heading = "Check options", about = None, long_about = None)]
pub struct CheckArgs {
/// Check source files from specified paths.
#[serde(skip)]
pub paths: Option<Vec<PathBuf>>,

#[command(flatten)]
#[serde(flatten)]
pub build: BuildOpts,

#[command(flatten)]
#[serde(skip)]
pub watch: WatchArgs,
}

impl CheckArgs {
pub async fn run(self) -> Result<()> {
let mut config = self.load_config()?;

if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
{
// need to re-configure here to also catch additional remappings
config = self.load_config()?;
}

let project = config.project()?;

// Collect sources to check if subdirectories specified.
let mut files = vec![];
if let Some(paths) = &self.paths {
for path in paths {
let joined = project.root().join(path);
let path = if joined.exists() { &joined } else { path };
files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
}
if files.is_empty() {
eyre::bail!("No source files found in specified check paths.")
}
}

// Run check using Solar for Solidity files
self.check_solidity(&project, &config, files.as_slice())?;

if !shell::is_json() {
sh_println!("Check completed successfully")?;
}

Ok(())
}

fn check_solidity(
&self,
project: &Project,
config: &Config,
target_files: &[PathBuf],
) -> Result<()> {
let sources = if target_files.is_empty() {
project.paths.read_input_files()?
} else {
let mut sources = Sources::new();
for path in target_files {
let canonical = dunce::canonicalize(path)?;
let source = Source::read(&canonical)?;
sources.insert(canonical, source);
}
sources
};

let sources_by_version =
Graph::<MultiCompilerParser>::resolve_sources(&project.paths, sources)?
.into_sources_by_version(project)?;

for (lang, versions) in sources_by_version.sources {
// Only check Solidity sources
if lang != MultiCompilerLanguage::Solc(SolcLanguage::Solidity) {
continue;
}

for (version, sources, _) in versions {
let vinput = SolcVersionedInput::build(
sources,
config.solc_settings()?,
SolcLanguage::Solidity,
version.clone(),
);

let mut sess = solar::interface::Session::builder().with_stderr_emitter().build();
sess.dcx.set_flags_mut(|flags| flags.track_diagnostics = false);

let mut compiler = Compiler::new(sess);

let result = compiler.enter_mut(|compiler| -> Result<()> {
let mut pcx = compiler.parse();
configure_pcx_from_solc(&mut pcx, &project.paths, &vinput, true);
pcx.parse();

let _ = compiler.lower_asts();

Ok(())
});

if compiler.sess().dcx.has_errors().is_err() {
eyre::bail!("Check failed for Solidity version {}", version);
}

result?;
}
}

Ok(())
}

/// Returns the `Project` for the current workspace
pub fn project(&self) -> Result<Project> {
self.build.project()
}

/// Returns whether `CheckArgs` was configured with `--watch`
pub fn is_watch(&self) -> bool {
self.watch.watch.is_some()
}

/// Returns the [`watchexec::Config`] necessary to bootstrap a new watch loop.
pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
self.watch.watchexec_config(|| {
let config = self.load_config()?;
let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
Ok([config.src, config.test, config.script, foundry_toml])
})
}
}

// Make this args a `figment::Provider` so that it can be merged into the `Config`
impl Provider for CheckArgs {
fn metadata(&self) -> Metadata {
Metadata::named("Check Args Provider")
}

fn data(&self) -> Result<figment::value::Map<Profile, figment::value::Dict>, figment::Error> {
let value = Value::serialize(self)?;
let error = InvalidType(value.to_actual(), "map".into());
let dict = value.into_dict().ok_or(error)?;

Ok(Map::from([(Config::selected_profile(), dict)]))
}
}
1 change: 1 addition & 0 deletions crates/forge/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod bind;
pub mod bind_json;
pub mod build;
pub mod cache;
pub mod check;
pub mod clone;
pub mod compiler;
pub mod config;
Expand Down
9 changes: 8 additions & 1 deletion crates/forge/src/cmd/watch.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{
build::BuildArgs, coverage::CoverageArgs, doc::DocArgs, fmt::FmtArgs,
build::BuildArgs, check::CheckArgs, coverage::CoverageArgs, doc::DocArgs, fmt::FmtArgs,
snapshot::GasSnapshotArgs, test::TestArgs,
};
use alloy_primitives::map::HashSet;
Expand Down Expand Up @@ -283,6 +283,13 @@ pub async fn watch_build(args: BuildArgs) -> Result<()> {
run(config).await
}

/// Executes a [`Watchexec`] that listens for changes in the project's src dir and reruns `forge
/// check`
pub async fn watch_check(args: CheckArgs) -> Result<()> {
let config = args.watchexec_config()?;
run(config).await
}

/// Executes a [`Watchexec`] that listens for changes in the project's src dir and reruns `forge
/// snapshot`
pub async fn watch_gas_snapshot(args: GasSnapshotArgs) -> Result<()> {
Expand Down
18 changes: 13 additions & 5 deletions crates/forge/src/opts.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::cmd::{
bind::BindArgs, bind_json, build::BuildArgs, cache::CacheArgs, clone::CloneArgs,
compiler::CompilerArgs, config, coverage, create::CreateArgs, doc::DocArgs, eip712, flatten,
fmt::FmtArgs, geiger, generate, init::InitArgs, inspect, install::InstallArgs, lint::LintArgs,
remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands, snapshot,
soldeer, test, tree, update,
bind::BindArgs, bind_json, build::BuildArgs, cache::CacheArgs, check::CheckArgs,
clone::CloneArgs, compiler::CompilerArgs, config, coverage, create::CreateArgs, doc::DocArgs,
eip712, flatten, fmt::FmtArgs, geiger, generate, init::InitArgs, inspect, install::InstallArgs,
lint::LintArgs, remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands,
snapshot, soldeer, test, tree, update,
};
use clap::{Parser, Subcommand, ValueHint};
use forge_script::ScriptArgs;
Expand Down Expand Up @@ -50,6 +50,14 @@ pub enum ForgeSubcommand {
#[command(visible_aliases = ["b", "compile"])]
Build(BuildArgs),

/// Check the project's smart contracts for errors.
///
/// Performs parsing, lowering and semantic analysis without codegen and optimizer steps.
/// This is faster than `build` but cannot guarantee that code will compile due to
/// skipped stack scheduling.
#[command(visible_alias = "ch")]
Check(CheckArgs),

/// Clone a contract from Etherscan.
Clone(CloneArgs),

Expand Down
89 changes: 89 additions & 0 deletions crates/forge/tests/cli/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use foundry_test_utils::{forgetest, str};

// Test that `forge check` succeeds on a valid project
forgetest_init!(check_basic, |prj, cmd| {
cmd.args(["check"]).assert_success().stdout_eq(str![[r#"
Check completed successfully

"#]]);
});

// Test that `forge check` detects syntax errors
forgetest!(check_syntax_error, |prj, cmd| {
prj.add_source(
"SyntaxError",
r"
contract SyntaxError {
uint256 public value = 10 // missing semicolon

function test() public {}
}
",
);

cmd.args(["check"]).assert_failure().stderr_eq(str![["
error: expected one of `(`, `.`, `;`, `?`, `[`, or `{`, found keyword `function`
[..]
[..]
[..]
[..]
[..]
[..]
[..]

Error: Check failed for Solidity version [..]

"]]);
});

// Test that `forge check` detects undefined symbols
forgetest!(check_undefined_symbol, |prj, cmd| {
prj.add_source(
"UndefinedVar",
r"
contract UndefinedVar {
function test() public pure returns (uint256) {
return undefinedVariable;
}
}
",
);

cmd.args(["check"]).assert_failure().stderr_eq(str![["
error: unresolved symbol `undefinedVariable`
[..]
[..]
[..]
[..]

Error: Check failed for Solidity version [..]

"]]);
});

// Test that `forge check` can check specific paths
forgetest!(check_specific_path, |prj, cmd| {
prj.add_source(
"Good",
r"
contract Good {
uint256 public value;
}
",
);

prj.add_source(
"Bad",
r"
contract Bad {
uint256 public value = 10 // missing semicolon
}
",
);

// Check only the good file should succeed
cmd.args(["check", "src/Good.sol"]).assert_success().stdout_eq(str![[r#"
Check completed successfully

"#]]);
});
1 change: 1 addition & 0 deletions crates/forge/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod bind;
mod bind_json;
mod build;
mod cache;
mod check;
mod cmd;
mod compiler;
mod config;
Expand Down
Loading