Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 1 addition & 7 deletions crates/forge/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,7 @@ pub fn run_command(args: Forge) -> Result<()> {
ForgeSubcommand::Flatten(cmd) => cmd.run(),
ForgeSubcommand::Inspect(cmd) => cmd.run(),
ForgeSubcommand::Tree(cmd) => cmd.run(),
ForgeSubcommand::Geiger(cmd) => {
let n = cmd.run()?;
if n > 0 {
std::process::exit(n as i32);
}
Ok(())
}
ForgeSubcommand::Geiger(cmd) => cmd.run(),
ForgeSubcommand::Doc(cmd) => {
if cmd.is_watch() {
global.block_on(watch::watch_doc(cmd))
Expand Down
161 changes: 29 additions & 132 deletions crates/forge/src/cmd/geiger.rs
Original file line number Diff line number Diff line change
@@ -1,162 +1,59 @@
use clap::{Parser, ValueHint};
use eyre::{Result, WrapErr};
use foundry_cli::utils::LoadConfig;
use foundry_compilers::{Graph, resolver::parse::SolData};
use foundry_config::{Config, impl_figment_convert_basic};
use itertools::Itertools;
use solar_parse::{ast, ast::visit::Visit, interface::Session};
use std::{
ops::ControlFlow,
path::{Path, PathBuf},
};
use eyre::Result;
use foundry_cli::opts::BuildOpts;
use foundry_config::impl_figment_convert;
use std::path::PathBuf;

/// CLI arguments for `forge geiger`.
///
/// This command is an alias for `forge lint --only-lint unsafe-cheatcode`
/// and detects usage of unsafe cheat codes in a project and its dependencies.
#[derive(Clone, Debug, Parser)]
pub struct GeigerArgs {
/// Paths to files or directories to detect.
#[arg(
conflicts_with = "root",
value_hint = ValueHint::FilePath,
value_name = "PATH",
num_args(1..),
num_args(0..)
)]
paths: Vec<PathBuf>,

/// The project's root path.
///
/// By default root of the Git repository, if in one,
/// or the current working directory.
#[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
root: Option<PathBuf>,

/// Globs to ignore.
#[arg(
long,
value_hint = ValueHint::FilePath,
value_name = "PATH",
num_args(1..),
)]
ignore: Vec<PathBuf>,

#[arg(long, hide = true)]
check: bool,

#[arg(long, hide = true)]
full: bool,

#[command(flatten)]
build: BuildOpts,
}

impl_figment_convert_basic!(GeigerArgs);
impl_figment_convert!(GeigerArgs, build);

impl GeigerArgs {
pub fn sources(&self, config: &Config) -> Result<Vec<PathBuf>> {
let cwd = std::env::current_dir()?;

let mut sources: Vec<PathBuf> = {
if self.paths.is_empty() {
let paths = config.project_paths();
Graph::<SolData>::resolve(&paths)?
.files()
.keys()
.filter(|f| !paths.has_library_ancestor(f))
.cloned()
.collect()
} else {
self.paths
.iter()
.flat_map(|path| foundry_common::fs::files_with_ext(path, "sol"))
.unique()
.collect()
}
};

sources.retain_mut(|path| {
let abs_path = if path.is_absolute() { path.clone() } else { cwd.join(&path) };
*path = abs_path.strip_prefix(&cwd).unwrap_or(&abs_path).to_path_buf();
!self.ignore.iter().any(|ignore| {
if ignore.is_absolute() {
abs_path.starts_with(ignore)
} else {
abs_path.starts_with(cwd.join(ignore))
}
})
});

Ok(sources)
}

pub fn run(self) -> Result<usize> {
pub fn run(self) -> Result<()> {
// Deprecated flags warnings
if self.check {
sh_warn!("`--check` is deprecated as it's now the default behavior\n")?;
}
if self.full {
sh_warn!("`--full` is deprecated as reports are not generated anymore\n")?;
}

let config = self.load_config()?;
let sources = self.sources(&config).wrap_err("Failed to resolve files")?;

if config.ffi {
sh_warn!("FFI enabled\n")?;
}

let mut sess = Session::builder().with_stderr_emitter().build();
sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
let unsafe_cheatcodes = &[
"ffi".to_string(),
"readFile".to_string(),
"readLine".to_string(),
"writeFile".to_string(),
"writeLine".to_string(),
"removeFile".to_string(),
"closeFile".to_string(),
"setEnv".to_string(),
"deriveKey".to_string(),
];
Ok(sess
.enter(|| sources.iter().map(|file| lint_file(&sess, unsafe_cheatcodes, file)).sum()))
}
}

fn lint_file(sess: &Session, unsafe_cheatcodes: &[String], path: &Path) -> usize {
try_lint_file(sess, unsafe_cheatcodes, path).unwrap_or(0)
}

fn try_lint_file(
sess: &Session,
unsafe_cheatcodes: &[String],
path: &Path,
) -> solar_parse::interface::Result<usize> {
let arena = solar_parse::ast::Arena::new();
let mut parser = solar_parse::Parser::from_file(sess, &arena, path)?;
let ast = parser.parse_file().map_err(|e| e.emit())?;
let mut visitor = Visitor::new(sess, unsafe_cheatcodes);
let _ = visitor.visit_source_unit(&ast);
Ok(visitor.count)
}

struct Visitor<'a> {
sess: &'a Session,
count: usize,
unsafe_cheatcodes: &'a [String],
}

impl<'a> Visitor<'a> {
fn new(sess: &'a Session, unsafe_cheatcodes: &'a [String]) -> Self {
Self { sess, count: 0, unsafe_cheatcodes }
}
}

impl<'ast> Visit<'ast> for Visitor<'_> {
type BreakValue = solar_parse::interface::data_structures::Never;
sh_warn!(
"`forge geiger` is just an alias for `forge lint --only-lint unsafe-cheatcode`\n"
)?;

// Convert geiger command to lint command with specific lint filter
let lint_args = crate::cmd::lint::LintArgs {
paths: self.paths,
severity: None,
lint: Some(vec!["unsafe-cheatcode".to_string()]),
json: false,
build: self.build,
};

fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
if let ast::ExprKind::Call(lhs, _args) = &expr.kind
&& let ast::ExprKind::Member(_lhs, member) = &lhs.kind
&& self.unsafe_cheatcodes.iter().any(|c| c.as_str() == member.as_str())
{
let msg = format!("usage of unsafe cheatcode `vm.{member}`");
self.sess.dcx.err(msg).span(member.span).emit();
self.count += 1;
}
self.walk_expr(expr)
// Run the lint command with the geiger-specific configuration
lint_args.run()
}
}
10 changes: 5 additions & 5 deletions crates/forge/src/cmd/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,25 @@ use std::path::PathBuf;
pub struct LintArgs {
/// Path to the file to be checked. Overrides the `ignore` project config.
#[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
paths: Vec<PathBuf>,
pub(crate) paths: Vec<PathBuf>,

/// Specifies which lints to run based on severity. Overrides the `severity` project config.
///
/// Supported values: `high`, `med`, `low`, `info`, `gas`.
#[arg(long, value_name = "SEVERITY", num_args(1..))]
severity: Option<Vec<Severity>>,
pub(crate) severity: Option<Vec<Severity>>,

/// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). Overrides the
/// `exclude_lints` project config.
#[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))]
lint: Option<Vec<String>>,
pub(crate) lint: Option<Vec<String>>,

/// Activates the linter's JSON formatter (rustc-compatible).
#[arg(long)]
json: bool,
pub(crate) json: bool,

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

foundry_config::impl_figment_convert!(LintArgs, build);
Expand Down
2 changes: 2 additions & 0 deletions crates/forge/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ pub enum ForgeSubcommand {
Tree(tree::TreeArgs),

/// Detects usage of unsafe cheat codes in a project and its dependencies.
///
/// This is an alias for `forge lint --only-lint unsafe-cheatcode`.
Geiger(geiger::GeigerArgs),

/// Generate documentation for the project.
Expand Down
96 changes: 54 additions & 42 deletions crates/forge/tests/cli/geiger.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
forgetest!(call, |prj, cmd| {
prj.add_source(
"call.sol",
forgetest_init!(call, |prj, cmd| {
prj.add_test(
"call.t.sol",
r#"
import {Test} from "forge-std/Test.sol";

contract A is Test {
function do_ffi() public {
string[] memory inputs = new string[](1);
Expand All @@ -12,22 +14,25 @@ forgetest!(call, |prj, cmd| {
)
.unwrap();

cmd.arg("geiger").assert_code(1).stderr_eq(str![[r#"
error: usage of unsafe cheatcode `vm.ffi`
[FILE]:7:20
cmd.arg("geiger").assert_success().stderr_eq(str![[r#"
...
note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations
[FILE]:9:20
|
7 | vm.ffi(inputs);
| ^^^
9 | vm.ffi(inputs);
| ---
|


= help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode
...
"#]]);
});

forgetest!(assignment, |prj, cmd| {
prj.add_source(
"assignment.sol",
forgetest_init!(assignment, |prj, cmd| {
prj.add_test(
"assignment.t.sol",
r#"
import {Test} from "forge-std/Test.sol";

contract A is Test {
function do_ffi() public {
string[] memory inputs = new string[](1);
Expand All @@ -38,24 +43,28 @@ forgetest!(assignment, |prj, cmd| {
)
.unwrap();

cmd.arg("geiger").assert_code(1).stderr_eq(str![[r#"
error: usage of unsafe cheatcode `vm.ffi`
[FILE]:7:34
cmd.arg("geiger").assert_success().stderr_eq(str![[r#"
...
note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations
[FILE]:9:34
|
7 | bytes stuff = vm.ffi(inputs);
| ^^^
9 | bytes stuff = vm.ffi(inputs);
| ---
|


= help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode
...
"#]]);
});

forgetest!(exit_code, |prj, cmd| {
prj.add_source(
"multiple.sol",
forgetest_init!(exit_code, |prj, cmd| {
prj.add_test(
"multiple.t.sol",
r#"
import {Test} from "forge-std/Test.sol";

contract A is Test {
function do_ffi() public {
string[] memory inputs = new string[](1);
vm.ffi(inputs);
vm.ffi(inputs);
vm.ffi(inputs);
Expand All @@ -65,28 +74,31 @@ forgetest!(exit_code, |prj, cmd| {
)
.unwrap();

cmd.arg("geiger").assert_code(3).stderr_eq(str![[r#"
error: usage of unsafe cheatcode `vm.ffi`
[FILE]:6:20
|
6 | vm.ffi(inputs);
| ^^^
|

error: usage of unsafe cheatcode `vm.ffi`
[FILE]:7:20
|
7 | vm.ffi(inputs);
| ^^^
|

error: usage of unsafe cheatcode `vm.ffi`
[FILE]:8:20
cmd.arg("geiger").assert_success().stderr_eq(str![[r#"
...
note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations
[FILE]:9:20
|
8 | vm.ffi(inputs);
| ^^^
9 | vm.ffi(inputs);
| ---
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode

note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations
[FILE]:10:20
|
10 | vm.ffi(inputs);
| ---
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode

note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations
[FILE]:11:20
|
11 | vm.ffi(inputs);
| ---
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode
...
"#]]);
});
Loading
Loading