From f6b709651a2b0fe65e0c5c701dc4b6dea43e971c Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 21:09:32 -0500 Subject: [PATCH 001/107] add lint cmd, variable lints --- Cargo.lock | 9 +++ Cargo.toml | 2 + crates/forge/bin/cmd/lint.rs | 145 +++++++++++++++++++++++++++++++++++ crates/lint/Cargo.toml | 19 +++++ crates/lint/src/lib.rs | 7 ++ crates/lint/src/qa.rs | 91 ++++++++++++++++++++++ 6 files changed, 273 insertions(+) create mode 100644 crates/forge/bin/cmd/lint.rs create mode 100644 crates/lint/Cargo.toml create mode 100644 crates/lint/src/lib.rs create mode 100644 crates/lint/src/qa.rs diff --git a/Cargo.lock b/Cargo.lock index d4027c358f686..9bbe60530f3a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3552,6 +3552,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "forge-lint" +version = "0.3.0" +dependencies = [ + "regex", + "solar-ast", + "solar-parse", +] + [[package]] name = "forge-script" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index b6f4fef690d59..3c95cac0e2397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ members = [ "crates/script-sequence/", "crates/macros/", "crates/test-utils/", + "crates/lint/", + ] resolver = "2" diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs new file mode 100644 index 0000000000000..58928b186638f --- /dev/null +++ b/crates/forge/bin/cmd/lint.rs @@ -0,0 +1,145 @@ +use clap::{Parser, ValueHint}; +use eyre::{Context, Result}; +use forge_fmt::{format_to, parse}; +use foundry_cli::utils::{FoundryPathExt, LoadConfig}; +use foundry_common::fs; +use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; +use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; +use rayon::prelude::*; +use similar::{ChangeTag, TextDiff}; +use solar_ast::{ast, interface::Session}; +use std::{ + fmt::{self, Write}, + io, + io::{Read, Write as _}, + path::{Path, PathBuf}, +}; +use yansi::{Color, Paint, Style}; + +/// CLI arguments for `forge fmt`. +#[derive(Clone, Debug, Parser)] +pub struct LintArgs { + /// Path to the file, directory or '-' to read from stdin. + #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))] + paths: Vec, + + /// 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, +} + +impl_figment_convert_basic!(LintArgs); + +impl LintArgs { + pub fn run(self) -> Result<()> { + let config = self.try_load_config_emit_warnings()?; + + // Expand ignore globs and canonicalize from the get go + let ignored = expand_globs(&config.root, config.fmt.ignore.iter())? + .iter() + .flat_map(foundry_common::fs::canonicalize_path) + .collect::>(); + + let cwd = std::env::current_dir()?; + + // TODO: This logic is borrowed from `forge fmt`. This can be packaged and reused + let input = match &self.paths[..] { + [] => { + // Retrieve the project paths, and filter out the ignored ones. + let project_paths: Vec = config + .project_paths::() + .input_files_iter() + .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p)))) + .collect(); + Input::Paths(project_paths) + } + [one] if one == Path::new("-") => { + let mut s = String::new(); + io::stdin().read_to_string(&mut s).expect("Failed to read from stdin"); + Input::Stdin(s) + } + paths => { + let mut inputs = Vec::with_capacity(paths.len()); + for path in paths { + if !ignored.is_empty() + && ((path.is_absolute() && ignored.contains(path)) + || ignored.contains(&cwd.join(path))) + { + continue; + } + + if path.is_dir() { + inputs.extend(foundry_compilers::utils::source_files_iter( + path, + SOLC_EXTENSIONS, + )); + } else if path.is_sol() { + inputs.push(path.to_path_buf()); + } else { + warn!("Cannot process path {}", path.display()); + } + } + Input::Paths(inputs) + } + }; + + let lints = match input { + Input::Stdin(source) => { + // Create a new session with a buffer emitter. + // This is required to capture the emitted diagnostics and to return them at the end. + let sess = Session::builder() + .with_buffer_emitter(solar::interface::ColorChoice::Auto) + .build(); + + // Enter the context and parse the file. + let _ = sess.enter(|| -> solar::interface::Result<()> { + // Set up the parser. + let arena = ast::Arena::new(); + + let mut parser = + solar_parse::Parser::from_file(&sess, &arena, &Path::new(&source)) + .expect("TODO:"); + + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); + + Ok(()) + }); + + todo!("lint"); + } + + Input::Paths(paths) => { + if paths.is_empty() { + sh_warn!( + "Nothing to lint.\n\ + HINT: If you are working outside of the project, \ + try providing paths to your source files: `forge fmt `" + )?; + return Ok(()); + } + + // TODO: rayon + + todo!("lint"); + // paths + // .par_iter() + // .map(|path| { + // let source = fs::read_to_string(path)?; + // }) + // .collect() + } + }; + + Ok(()) + } +} + +#[derive(Debug)] +enum Input { + Stdin(String), + Paths(Vec), +} diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml new file mode 100644 index 0000000000000..d11017c6569d6 --- /dev/null +++ b/crates/lint/Cargo.toml @@ -0,0 +1,19 @@ + +[package] +name = "forge-lint" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +solar-parse.workspace = true +solar-ast.workspace = true +regex = "1.11" diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs new file mode 100644 index 0000000000000..575826fd2015f --- /dev/null +++ b/crates/lint/src/lib.rs @@ -0,0 +1,7 @@ +pub mod qa; + +pub struct ForgeLint { + //TODO: settings +} + +impl ForgeLint {} diff --git a/crates/lint/src/qa.rs b/crates/lint/src/qa.rs new file mode 100644 index 0000000000000..417e47bbd7b91 --- /dev/null +++ b/crates/lint/src/qa.rs @@ -0,0 +1,91 @@ +use regex::Regex; +use solar_ast::{ + ast::{FunctionHeader, ItemStruct, Span, VariableDefinition}, + visit::Visit, +}; + +pub struct VariableCamelCase { + items: Vec, +} + +impl<'ast> Visit<'ast> for VariableCamelCase { + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + if let Some(mutability) = var.mutability { + if !mutability.is_constant() && !mutability.is_immutable() { + if let Some(name) = var.name { + if !is_camel_case(name.as_str()) { + self.items.push(var.span); + } + } + } + } + } +} + +pub struct VariableCapsCase { + items: Vec, +} + +impl<'ast> Visit<'ast> for VariableCapsCase { + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + if let Some(mutability) = var.mutability { + if mutability.is_constant() || mutability.is_immutable() { + if let Some(name) = var.name { + if !is_caps_case(name.as_str()) { + self.items.push(var.span); + } + } + } + } + } +} + +pub struct VariablePascalCase { + items: Vec, +} + +impl<'ast> Visit<'ast> for VariablePascalCase { + fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { + if !is_pascal_case(strukt.name.as_str()) { + self.items.push(strukt.name.span); + } + } + + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + if let Some(mutability) = var.mutability { + if mutability.is_constant() || mutability.is_immutable() { + if let Some(name) = var.name { + if !is_caps_case(name.as_str()) { + self.items.push(var.span); + } + } + } + } + } +} + +pub struct FunctionCamelCase { + items: Vec, +} + +impl<'ast> Visit<'ast> for FunctionCamelCase { + //TODO: visit item +} + +// Check if a string is camelCase +pub fn is_camel_case(s: &str) -> bool { + let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); + re.is_match(s) && s.chars().any(|c| c.is_uppercase()) +} + +// Check if a string is PascalCase +pub fn is_pascal_case(s: &str) -> bool { + let re = Regex::new(r"^[A-Z0-9][a-zA-Z0-9]*$").unwrap(); + re.is_match(s) +} + +// Check if a string is SCREAMING_SNAKE_CASE +pub fn is_caps_case(s: &str) -> bool { + let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); + re.is_match(s) && s.contains('_') +} From a9071476f26572d8fc52ae54fb6941ff1558d2a9 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 21:14:18 -0500 Subject: [PATCH 002/107] wip --- crates/lint/src/lib.rs | 28 ++++++++++++++++++++++++++++ crates/lint/src/qa.rs | 18 ++---------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 575826fd2015f..93433027f2bde 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,7 +1,35 @@ pub mod qa; +use solar_ast::ast::Span; + pub struct ForgeLint { //TODO: settings } impl ForgeLint {} + +macro_rules! declare_lints { + ($($name:ident),* $(,)?) => { + $( + pub struct $name { + pub items: Vec, + } + + impl $name { + pub fn new() -> Self { + Self { items: Vec::new() } + } + } + )* + }; +} + +declare_lints!( + //Optimizations + // Vunlerabilities + // QA + VariableCamelCase, + VariableCapsCase, + VariablePascalCase, + FunctionCamelCase, +); diff --git a/crates/lint/src/qa.rs b/crates/lint/src/qa.rs index 417e47bbd7b91..d76177b83af31 100644 --- a/crates/lint/src/qa.rs +++ b/crates/lint/src/qa.rs @@ -1,12 +1,10 @@ use regex::Regex; use solar_ast::{ - ast::{FunctionHeader, ItemStruct, Span, VariableDefinition}, + ast::{ItemStruct, Span, VariableDefinition}, visit::Visit, }; -pub struct VariableCamelCase { - items: Vec, -} +use crate::{FunctionCamelCase, VariableCamelCase, VariableCapsCase, VariablePascalCase}; impl<'ast> Visit<'ast> for VariableCamelCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { @@ -22,10 +20,6 @@ impl<'ast> Visit<'ast> for VariableCamelCase { } } -pub struct VariableCapsCase { - items: Vec, -} - impl<'ast> Visit<'ast> for VariableCapsCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { if let Some(mutability) = var.mutability { @@ -40,10 +34,6 @@ impl<'ast> Visit<'ast> for VariableCapsCase { } } -pub struct VariablePascalCase { - items: Vec, -} - impl<'ast> Visit<'ast> for VariablePascalCase { fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { if !is_pascal_case(strukt.name.as_str()) { @@ -64,10 +54,6 @@ impl<'ast> Visit<'ast> for VariablePascalCase { } } -pub struct FunctionCamelCase { - items: Vec, -} - impl<'ast> Visit<'ast> for FunctionCamelCase { //TODO: visit item } From e43414eeee9e92eb858b4bc8a3660b54809d578e Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 21:15:35 -0500 Subject: [PATCH 003/107] wip --- crates/lint/src/qa.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/lint/src/qa.rs b/crates/lint/src/qa.rs index d76177b83af31..1abd3905e1de2 100644 --- a/crates/lint/src/qa.rs +++ b/crates/lint/src/qa.rs @@ -1,6 +1,6 @@ use regex::Regex; use solar_ast::{ - ast::{ItemStruct, Span, VariableDefinition}, + ast::{ItemStruct, VariableDefinition}, visit::Visit, }; @@ -17,6 +17,7 @@ impl<'ast> Visit<'ast> for VariableCamelCase { } } } + // TODO: Walk } } @@ -31,6 +32,7 @@ impl<'ast> Visit<'ast> for VariableCapsCase { } } } + // TODO: Walk } } @@ -39,18 +41,8 @@ impl<'ast> Visit<'ast> for VariablePascalCase { if !is_pascal_case(strukt.name.as_str()) { self.items.push(strukt.name.span); } - } - fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { - if let Some(mutability) = var.mutability { - if mutability.is_constant() || mutability.is_immutable() { - if let Some(name) = var.name { - if !is_caps_case(name.as_str()) { - self.items.push(var.span); - } - } - } - } + // TODO: Walk } } From bd3f2a5797af819cada47d9b910a470506933753 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 22:01:29 -0500 Subject: [PATCH 004/107] wip --- crates/lint/Cargo.toml | 4 ++++ crates/lint/src/lib.rs | 5 +++-- crates/lint/src/qa.rs | 38 ++++++++++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index d11017c6569d6..c5e7331200cfd 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -16,4 +16,8 @@ workspace = true [dependencies] solar-parse.workspace = true solar-ast.workspace = true +solar-data-structures = { git = "https://github.com/paradigmxyz/solar.git", rev = "bb81e5d4cc8ab28568a163ea6b2506c3a0d1a6d3", features = [ + "nightly", +] } + regex = "1.11" diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 93433027f2bde..ac3cd0780bcce 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,6 +1,6 @@ -pub mod qa; +use solar_ast::Span; -use solar_ast::ast::Span; +pub mod qa; pub struct ForgeLint { //TODO: settings @@ -24,6 +24,7 @@ macro_rules! declare_lints { }; } +// TODO: update macro to include descriptions. Group by opts, vulns, qa declare_lints!( //Optimizations // Vunlerabilities diff --git a/crates/lint/src/qa.rs b/crates/lint/src/qa.rs index 1abd3905e1de2..b5a23c950f029 100644 --- a/crates/lint/src/qa.rs +++ b/crates/lint/src/qa.rs @@ -1,13 +1,19 @@ +use std::ops::ControlFlow; + use regex::Regex; -use solar_ast::{ - ast::{ItemStruct, VariableDefinition}, - visit::Visit, -}; + +use solar_ast::{visit::Visit, ItemStruct, VariableDefinition}; +use solar_data_structures::Never; use crate::{FunctionCamelCase, VariableCamelCase, VariableCapsCase, VariablePascalCase}; impl<'ast> Visit<'ast> for VariableCamelCase { - fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + type BreakValue = Never; + + fn visit_variable_definition( + &mut self, + var: &'ast VariableDefinition<'ast>, + ) -> ControlFlow { if let Some(mutability) = var.mutability { if !mutability.is_constant() && !mutability.is_immutable() { if let Some(name) = var.name { @@ -17,12 +23,17 @@ impl<'ast> Visit<'ast> for VariableCamelCase { } } } - // TODO: Walk + self.walk_variable_definition(var) } } impl<'ast> Visit<'ast> for VariableCapsCase { - fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + type BreakValue = Never; + + fn visit_variable_definition( + &mut self, + var: &'ast VariableDefinition<'ast>, + ) -> ControlFlow { if let Some(mutability) = var.mutability { if mutability.is_constant() || mutability.is_immutable() { if let Some(name) = var.name { @@ -32,21 +43,28 @@ impl<'ast> Visit<'ast> for VariableCapsCase { } } } - // TODO: Walk + self.walk_variable_definition(var) } } impl<'ast> Visit<'ast> for VariablePascalCase { - fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { + type BreakValue = Never; + + fn visit_item_struct( + &mut self, + strukt: &'ast ItemStruct<'ast>, + ) -> ControlFlow { if !is_pascal_case(strukt.name.as_str()) { self.items.push(strukt.name.span); } - // TODO: Walk + self.walk_item_struct(strukt) } } impl<'ast> Visit<'ast> for FunctionCamelCase { + type BreakValue = Never; + //TODO: visit item } From d7048ae60ab2cc65576f12d0e20dde1292b7bf66 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 22:14:48 -0500 Subject: [PATCH 005/107] wip --- crates/lint/Cargo.toml | 4 ---- crates/lint/src/lib.rs | 14 +++++++++++--- crates/lint/src/qa.rs | 37 ++++++++++--------------------------- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index c5e7331200cfd..d11017c6569d6 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -16,8 +16,4 @@ workspace = true [dependencies] solar-parse.workspace = true solar-ast.workspace = true -solar-data-structures = { git = "https://github.com/paradigmxyz/solar.git", rev = "bb81e5d4cc8ab28568a163ea6b2506c3a0d1a6d3", features = [ - "nightly", -] } - regex = "1.11" diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index ac3cd0780bcce..5798ac8663b4e 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,16 +1,24 @@ -use solar_ast::Span; - pub mod qa; +use solar_ast::ast::Span; + pub struct ForgeLint { - //TODO: settings + pub lints: Vec, } impl ForgeLint {} macro_rules! declare_lints { ($($name:ident),* $(,)?) => { + #[derive(Debug)] + pub enum Lint { + $( + $name($name), + )* + } + $( + #[derive(Debug)] pub struct $name { pub items: Vec, } diff --git a/crates/lint/src/qa.rs b/crates/lint/src/qa.rs index b5a23c950f029..65a789e2bb23b 100644 --- a/crates/lint/src/qa.rs +++ b/crates/lint/src/qa.rs @@ -1,19 +1,14 @@ -use std::ops::ControlFlow; - use regex::Regex; -use solar_ast::{visit::Visit, ItemStruct, VariableDefinition}; -use solar_data_structures::Never; +use solar_ast::{ + ast::{ItemStruct, VariableDefinition}, + visit::Visit, +}; use crate::{FunctionCamelCase, VariableCamelCase, VariableCapsCase, VariablePascalCase}; impl<'ast> Visit<'ast> for VariableCamelCase { - type BreakValue = Never; - - fn visit_variable_definition( - &mut self, - var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow { + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { if let Some(mutability) = var.mutability { if !mutability.is_constant() && !mutability.is_immutable() { if let Some(name) = var.name { @@ -23,17 +18,12 @@ impl<'ast> Visit<'ast> for VariableCamelCase { } } } - self.walk_variable_definition(var) + self.walk_variable_definition(var); } } impl<'ast> Visit<'ast> for VariableCapsCase { - type BreakValue = Never; - - fn visit_variable_definition( - &mut self, - var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow { + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { if let Some(mutability) = var.mutability { if mutability.is_constant() || mutability.is_immutable() { if let Some(name) = var.name { @@ -43,28 +33,21 @@ impl<'ast> Visit<'ast> for VariableCapsCase { } } } - self.walk_variable_definition(var) + self.walk_variable_definition(var); } } impl<'ast> Visit<'ast> for VariablePascalCase { - type BreakValue = Never; - - fn visit_item_struct( - &mut self, - strukt: &'ast ItemStruct<'ast>, - ) -> ControlFlow { + fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { if !is_pascal_case(strukt.name.as_str()) { self.items.push(strukt.name.span); } - self.walk_item_struct(strukt) + self.walk_item_struct(strukt); } } impl<'ast> Visit<'ast> for FunctionCamelCase { - type BreakValue = Never; - //TODO: visit item } From 4d2722994d7e69d340c9f9f6064024de25b20383 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 22:43:42 -0500 Subject: [PATCH 006/107] wip --- Cargo.lock | 3 ++ Cargo.toml | 2 + crates/forge/Cargo.toml | 2 + crates/forge/bin/cmd/lint.rs | 57 ++--------------------- crates/forge/bin/cmd/mod.rs | 1 + crates/lint/Cargo.toml | 1 + crates/lint/src/lib.rs | 88 ++++++++++++++++++++++++++++++++++-- 7 files changed, 97 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bbe60530f3a4..3b74aace12a88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3454,6 +3454,7 @@ dependencies = [ "eyre", "forge-doc", "forge-fmt", + "forge-lint", "forge-script", "forge-script-sequence", "forge-sol-macro-gen", @@ -3493,6 +3494,7 @@ dependencies = [ "similar-asserts", "solang-parser", "solar-ast", + "solar-interface", "solar-parse", "soldeer-commands", "strum", @@ -3558,6 +3560,7 @@ version = "0.3.0" dependencies = [ "regex", "solar-ast", + "solar-interface", "solar-parse", ] diff --git a/Cargo.toml b/Cargo.toml index 3c95cac0e2397..561b74b325832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,7 @@ forge = { path = "crates/forge" } forge-doc = { path = "crates/doc" } forge-fmt = { path = "crates/fmt" } +forge-lint = { path = "crates/lint" } forge-verify = { path = "crates/verify" } forge-script = { path = "crates/script" } forge-sol-macro-gen = { path = "crates/sol-macro-gen" } @@ -176,6 +177,7 @@ foundry-fork-db = "0.9.0" solang-parser = "=0.3.3" solar-ast = { version = "=0.1.0", default-features = false } solar-parse = { version = "=0.1.0", default-features = false } +solar-interface = { version = "=0.1.0", default-features = false } ## revm revm = { version = "18.0.0", default-features = false } diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index cbbfa814daf12..e5a753775c3d9 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -52,6 +52,7 @@ chrono.workspace = true # bin forge-doc.workspace = true forge-fmt.workspace = true +forge-lint.workspace = true forge-verify.workspace = true forge-script.workspace = true forge-sol-macro-gen.workspace = true @@ -91,6 +92,7 @@ similar = { version = "2", features = ["inline"] } solang-parser.workspace = true solar-ast.workspace = true solar-parse.workspace = true +solar-interface.workspace = true strum = { workspace = true, features = ["derive"] } thiserror.workspace = true tokio = { workspace = true, features = ["time"] } diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 58928b186638f..4aac9a9009bbf 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,6 +1,7 @@ use clap::{Parser, ValueHint}; use eyre::{Context, Result}; use forge_fmt::{format_to, parse}; +use forge_lint::{ForgeLint, Input}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; use foundry_common::fs; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; @@ -8,6 +9,7 @@ use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; use rayon::prelude::*; use similar::{ChangeTag, TextDiff}; use solar_ast::{ast, interface::Session}; +use solar_interface::ColorChoice; use std::{ fmt::{self, Write}, io, @@ -45,7 +47,6 @@ impl LintArgs { let cwd = std::env::current_dir()?; - // TODO: This logic is borrowed from `forge fmt`. This can be packaged and reused let input = match &self.paths[..] { [] => { // Retrieve the project paths, and filter out the ignored ones. @@ -86,60 +87,8 @@ impl LintArgs { } }; - let lints = match input { - Input::Stdin(source) => { - // Create a new session with a buffer emitter. - // This is required to capture the emitted diagnostics and to return them at the end. - let sess = Session::builder() - .with_buffer_emitter(solar::interface::ColorChoice::Auto) - .build(); - - // Enter the context and parse the file. - let _ = sess.enter(|| -> solar::interface::Result<()> { - // Set up the parser. - let arena = ast::Arena::new(); - - let mut parser = - solar_parse::Parser::from_file(&sess, &arena, &Path::new(&source)) - .expect("TODO:"); - - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); - - Ok(()) - }); - - todo!("lint"); - } - - Input::Paths(paths) => { - if paths.is_empty() { - sh_warn!( - "Nothing to lint.\n\ - HINT: If you are working outside of the project, \ - try providing paths to your source files: `forge fmt `" - )?; - return Ok(()); - } - - // TODO: rayon - - todo!("lint"); - // paths - // .par_iter() - // .map(|path| { - // let source = fs::read_to_string(path)?; - // }) - // .collect() - } - }; + ForgeLint::new(input).lint(); Ok(()) } } - -#[derive(Debug)] -enum Input { - Stdin(String), - Paths(Vec), -} diff --git a/crates/forge/bin/cmd/mod.rs b/crates/forge/bin/cmd/mod.rs index 427b25fb0c50d..f3521cf8ffa3a 100644 --- a/crates/forge/bin/cmd/mod.rs +++ b/crates/forge/bin/cmd/mod.rs @@ -58,6 +58,7 @@ pub mod generate; pub mod init; pub mod inspect; pub mod install; +pub mod lint; pub mod remappings; pub mod remove; pub mod selectors; diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index d11017c6569d6..4af8302d41181 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -16,4 +16,5 @@ workspace = true [dependencies] solar-parse.workspace = true solar-ast.workspace = true +solar-interface.workspace = true regex = "1.11" diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 5798ac8663b4e..9419ed428e9a3 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,12 +1,73 @@ pub mod qa; -use solar_ast::ast::Span; +use std::path::{Path, PathBuf}; + +use solar_ast::{ + ast::{self, SourceUnit, Span}, + interface::{ColorChoice, Session}, + visit::Visit, +}; + +#[derive(Debug)] +pub enum Input { + Stdin(String), + Paths(Vec), +} pub struct ForgeLint { + pub input: Input, pub lints: Vec, } -impl ForgeLint {} +impl ForgeLint { + // TODO: Add config specifying which lints to run + pub fn new(input: Input) -> Self { + Self { input, lints: Lint::all() } + } + + pub fn lint(self) { + match self.input { + Input::Stdin(source) => { + // Create a new session with a buffer emitter. + // This is required to capture the emitted diagnostics and to return them at the + // end. + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + // Enter the context and parse the file. + let _ = sess.enter(|| -> solar_interface::Result<()> { + // Set up the parser. + let arena = ast::Arena::new(); + + let mut parser = + solar_parse::Parser::from_file(&sess, &arena, &Path::new(&source)) + .expect("TODO:"); + + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); + + for mut lint in self.lints { + lint.visit_source_unit(&ast); + } + + Ok(()) + }); + } + + Input::Paths(paths) => { + if paths.is_empty() { + // sh_warn!( + // "Nothing to lint.\n\ + // HINT: If you are working outside of the project, \ + // try providing paths to your source files: `forge fmt `" + // )?; + todo!(); + } + + todo!(); + } + }; + } +} macro_rules! declare_lints { ($($name:ident),* $(,)?) => { @@ -17,6 +78,27 @@ macro_rules! declare_lints { )* } + + impl Lint { + pub fn all() -> Vec { + vec![ + $( + Lint::$name($name::new()), + )* + ] + } + } + + impl<'ast> Visit<'ast> for Lint { + fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { + match self { + $( + Lint::$name(lint) => lint.visit_source_unit(source_unit), + )* + } + } + } + $( #[derive(Debug)] pub struct $name { @@ -32,7 +114,7 @@ macro_rules! declare_lints { }; } -// TODO: update macro to include descriptions. Group by opts, vulns, qa +// TODO: Group by opts, vulns, qa, add description for each lint declare_lints!( //Optimizations // Vunlerabilities From 1dfd008e1ddd671f116b8b8a20793e1b5dc1be82 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 23:29:11 -0500 Subject: [PATCH 007/107] wip --- crates/lint/Cargo.toml | 1 + crates/lint/src/lib.rs | 6 ++- crates/lint/src/optimization.rs | 21 ++++++++ crates/lint/src/vulnerability.rs | 54 +++++++++++++++++++ crates/lint/testdata/DivideBeforeMultiply.sol | 37 +++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 crates/lint/src/optimization.rs create mode 100644 crates/lint/src/vulnerability.rs create mode 100644 crates/lint/testdata/DivideBeforeMultiply.sol diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 4af8302d41181..54154d5eb9a46 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -17,4 +17,5 @@ workspace = true solar-parse.workspace = true solar-ast.workspace = true solar-interface.workspace = true +eyre.workspace = true regex = "1.11" diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 9419ed428e9a3..03d67b93ddb43 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,4 +1,6 @@ +pub mod optimization; pub mod qa; +pub mod vulnerability; use std::path::{Path, PathBuf}; @@ -100,7 +102,7 @@ macro_rules! declare_lints { } $( - #[derive(Debug)] + #[derive(Debug, Default)] pub struct $name { pub items: Vec, } @@ -117,7 +119,9 @@ macro_rules! declare_lints { // TODO: Group by opts, vulns, qa, add description for each lint declare_lints!( //Optimizations + Keccak256, // Vunlerabilities + DivideBeforeMultiply, // QA VariableCamelCase, VariableCapsCase, diff --git a/crates/lint/src/optimization.rs b/crates/lint/src/optimization.rs new file mode 100644 index 0000000000000..d75b0c0b6d59a --- /dev/null +++ b/crates/lint/src/optimization.rs @@ -0,0 +1,21 @@ +use solar_ast::{ + ast::{Item, ItemFunction, ItemKind}, + visit::Visit, +}; + +use crate::Keccak256; + +impl<'ast> Visit<'ast> for Keccak256 { + fn visit_item(&mut self, item: &'ast Item<'ast>) { + if let ItemKind::Function(ItemFunction { kind, header, body }) = &item.kind { + if let Some(name) = header.name { + // Use assembly to hash + if name.as_str() == "keccak256" { + self.items.push(item.span); + } + } + } + + self.walk_item(item); + } +} diff --git a/crates/lint/src/vulnerability.rs b/crates/lint/src/vulnerability.rs new file mode 100644 index 0000000000000..df9fbfd7b5cd4 --- /dev/null +++ b/crates/lint/src/vulnerability.rs @@ -0,0 +1,54 @@ +use solar_ast::{ + ast::{BinOp, Expr, ExprKind}, + visit::Visit, +}; + +use crate::DivideBeforeMultiply; + +impl<'ast> Visit<'ast> for DivideBeforeMultiply { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + if let ExprKind::Binary(expr_0, binop, expr_1) = &expr.kind {} + } +} + +mod test { + use std::{path::Path, vec}; + + use solar_ast::ast; + use solar_interface::{sym::assert, ColorChoice, Session}; + + use crate::ForgeLint; + + #[allow(unused)] + use super::*; + #[allow(unused)] + #[test] + fn test_divide_before_multiply() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = solar_parse::Parser::from_file( + &sess, + &arena, + Path::new("testdata/DivideBeforeMultiply.sol"), + ) + .expect("TODO:"); + + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); + + dbg!(&ast); + + let mut pattern = DivideBeforeMultiply::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.items.len(), 0); + + Ok(()) + }); + + Ok(()) + } +} diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol new file mode 100644 index 0000000000000..de6d805cd7b02 --- /dev/null +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -0,0 +1,37 @@ +contract Contract0 { + function arithmetic() public { + (1 / 2) * 3; // Unsafe + (1 * 2) / 3; // Safe + (1 / 2) * 3; // Unsafe + (1 * 2) / 3; // Safe + ((1 / 2) * 3) * 4; // Unsafe (x2) + ((1 * 2) / 3) * 4; // Unsafe + (1 / 2 / 3) * 4; // Unsafe + (1 / (2 + 3)) * 4; // Unsafe + (1 / 2 + 3) * 4; // Safe + (1 / 2 - 3) * 4; // Safe + (1 + 2 / 3) * 4; // Safe + (1 / 2 - 3) * 4; // Safe + ((1 / 2) % 3) * 4; // Safe + 1 / (2 * 3 + 3); // Safe + 1 / ((2 / 3) * 3); // Unsafe + 1 / ((2 * 3) + 3); // Safe + + uint256 x = 5; + x /= 2 * 3; // Unsafe + x /= (2 * 3); // Unsafe + x /= 2 * 3 - 4; // Unsafe + x /= (2 * 3) % 4; // Unsafe + x /= (2 * 3) | 4; // Unsafe + x /= (2 * 3) & 4; // Unsafe + x /= (2 * 3) ^ 4; // Unsafe + x /= (2 * 3) << 4; // Unsafe + x /= (2 * 3) >> 4; // Unsafe + x /= 3 % 4; // Safe + x /= 3 | 4; // Safe + x /= 3 & 4; // Safe + x /= 3 ^ 4; // Safe + x /= 3 << 4; // Safe + x /= 3 >> 4; // Safe + } +} From 9960ee8f15ed4ec2265b4e4631263e2b6e423389 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 22 Dec 2024 23:49:27 -0500 Subject: [PATCH 008/107] add keccak256 opt test --- Cargo.lock | 1 + crates/forge/bin/cmd/lint.rs | 12 +---- crates/forge/bin/main.rs | 1 + crates/forge/bin/opts.rs | 8 ++- crates/lint/src/optimization.rs | 51 ++++++++++++++++--- crates/lint/testdata/DivideBeforeMultiply.sol | 32 ------------ crates/lint/testdata/Keccak256.sol | 19 +++++++ 7 files changed, 72 insertions(+), 52 deletions(-) create mode 100644 crates/lint/testdata/Keccak256.sol diff --git a/Cargo.lock b/Cargo.lock index 3b74aace12a88..03eba681e1e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3558,6 +3558,7 @@ dependencies = [ name = "forge-lint" version = "0.3.0" dependencies = [ + "eyre", "regex", "solar-ast", "solar-interface", diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 4aac9a9009bbf..c98ec8d443051 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,22 +1,14 @@ use clap::{Parser, ValueHint}; -use eyre::{Context, Result}; -use forge_fmt::{format_to, parse}; +use eyre::Result; use forge_lint::{ForgeLint, Input}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; -use foundry_common::fs; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; -use rayon::prelude::*; -use similar::{ChangeTag, TextDiff}; -use solar_ast::{ast, interface::Session}; -use solar_interface::ColorChoice; use std::{ - fmt::{self, Write}, io, - io::{Read, Write as _}, + io::Read, path::{Path, PathBuf}, }; -use yansi::{Color, Paint, Style}; /// CLI arguments for `forge fmt`. #[derive(Clone, Debug, Parser)] diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index d60c1639a05a5..7d75be34b5d43 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -100,6 +100,7 @@ fn run() -> Result<()> { } } ForgeSubcommand::Fmt(cmd) => cmd.run(), + ForgeSubcommand::Lint(cmd) => cmd.run(), ForgeSubcommand::Config(cmd) => cmd.run(), ForgeSubcommand::Flatten(cmd) => cmd.run(), ForgeSubcommand::Inspect(cmd) => cmd.run(), diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index 380cb61d403a5..c1060e9c5c05e 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -2,8 +2,8 @@ use crate::cmd::{ bind::BindArgs, bind_json, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, compiler::CompilerArgs, config, coverage, create::CreateArgs, debug::DebugArgs, doc::DocArgs, eip712, flatten, fmt::FmtArgs, geiger, generate, init::InitArgs, inspect, install::InstallArgs, - remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands, snapshot, - soldeer, test, tree, update, + lint::LintArgs, remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands, + snapshot, soldeer, test, tree, update, }; use clap::{Parser, Subcommand, ValueHint}; use forge_script::ScriptArgs; @@ -144,6 +144,10 @@ pub enum ForgeSubcommand { /// Format Solidity source files. Fmt(FmtArgs), + /// Lint Solidity source files + #[command(visible_alias = "l")] + Lint(LintArgs), + /// Get specialized information about a smart contract. #[command(visible_alias = "in")] Inspect(inspect::InspectArgs), diff --git a/crates/lint/src/optimization.rs b/crates/lint/src/optimization.rs index d75b0c0b6d59a..fa2ead3aa9507 100644 --- a/crates/lint/src/optimization.rs +++ b/crates/lint/src/optimization.rs @@ -1,21 +1,56 @@ use solar_ast::{ - ast::{Item, ItemFunction, ItemKind}, + ast::{Expr, ExprKind}, visit::Visit, }; use crate::Keccak256; impl<'ast> Visit<'ast> for Keccak256 { - fn visit_item(&mut self, item: &'ast Item<'ast>) { - if let ItemKind::Function(ItemFunction { kind, header, body }) = &item.kind { - if let Some(name) = header.name { - // Use assembly to hash - if name.as_str() == "keccak256" { - self.items.push(item.span); + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + if let ExprKind::Call(expr, _) = &expr.kind { + if let ExprKind::Ident(ident) = &expr.kind { + if ident.name.as_str() == "keccak256" { + self.items.push(expr.span); } } } - self.walk_item(item); + self.walk_expr(expr); + } +} + +#[cfg(test)] +mod test { + use solar_ast::{ast, visit::Visit}; + use solar_interface::{ColorChoice, Session}; + use std::path::Path; + + use crate::Keccak256; + + #[test] + fn test_keccak256() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = + solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol")) + .expect("TODO:"); + + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); + + dbg!(&ast); + + let mut pattern = Keccak256::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.items.len(), 2); + + Ok(()) + }); + + Ok(()) } } diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol index de6d805cd7b02..3382e49189ccb 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.sol +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -1,37 +1,5 @@ contract Contract0 { function arithmetic() public { (1 / 2) * 3; // Unsafe - (1 * 2) / 3; // Safe - (1 / 2) * 3; // Unsafe - (1 * 2) / 3; // Safe - ((1 / 2) * 3) * 4; // Unsafe (x2) - ((1 * 2) / 3) * 4; // Unsafe - (1 / 2 / 3) * 4; // Unsafe - (1 / (2 + 3)) * 4; // Unsafe - (1 / 2 + 3) * 4; // Safe - (1 / 2 - 3) * 4; // Safe - (1 + 2 / 3) * 4; // Safe - (1 / 2 - 3) * 4; // Safe - ((1 / 2) % 3) * 4; // Safe - 1 / (2 * 3 + 3); // Safe - 1 / ((2 / 3) * 3); // Unsafe - 1 / ((2 * 3) + 3); // Safe - - uint256 x = 5; - x /= 2 * 3; // Unsafe - x /= (2 * 3); // Unsafe - x /= 2 * 3 - 4; // Unsafe - x /= (2 * 3) % 4; // Unsafe - x /= (2 * 3) | 4; // Unsafe - x /= (2 * 3) & 4; // Unsafe - x /= (2 * 3) ^ 4; // Unsafe - x /= (2 * 3) << 4; // Unsafe - x /= (2 * 3) >> 4; // Unsafe - x /= 3 % 4; // Safe - x /= 3 | 4; // Safe - x /= 3 & 4; // Safe - x /= 3 ^ 4; // Safe - x /= 3 << 4; // Safe - x /= 3 >> 4; // Safe } } diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol new file mode 100644 index 0000000000000..b908c5727d621 --- /dev/null +++ b/crates/lint/testdata/Keccak256.sol @@ -0,0 +1,19 @@ +contract Contract0 { + constructor(uint256 a, uint256 b) { + keccak256(abi.encodePacked(a, b)); + } + + function solidityHash(uint256 a, uint256 b) public view { + //unoptimized + keccak256(abi.encodePacked(a, b)); + } + + function assemblyHash(uint256 a, uint256 b) public view { + //optimized + assembly { + mstore(0x00, a) + mstore(0x20, b) + let hashedVal := keccak256(0x00, 0x40) + } + } +} From cac3f7a7b3538a3a374c7b26342d9e24fa500fed Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 00:19:14 -0500 Subject: [PATCH 009/107] wip --- crates/lint/src/optimization.rs | 1 - crates/lint/src/vulnerability.rs | 53 +++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/crates/lint/src/optimization.rs b/crates/lint/src/optimization.rs index fa2ead3aa9507..23f5140ffcab1 100644 --- a/crates/lint/src/optimization.rs +++ b/crates/lint/src/optimization.rs @@ -14,7 +14,6 @@ impl<'ast> Visit<'ast> for Keccak256 { } } } - self.walk_expr(expr); } } diff --git a/crates/lint/src/vulnerability.rs b/crates/lint/src/vulnerability.rs index df9fbfd7b5cd4..5acb127554cae 100644 --- a/crates/lint/src/vulnerability.rs +++ b/crates/lint/src/vulnerability.rs @@ -1,27 +1,58 @@ use solar_ast::{ - ast::{BinOp, Expr, ExprKind}, + ast::{BinOp, BinOpKind, Expr, ExprKind}, visit::Visit, }; use crate::DivideBeforeMultiply; - impl<'ast> Visit<'ast> for DivideBeforeMultiply { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - if let ExprKind::Binary(expr_0, binop, expr_1) = &expr.kind {} + // Check if the current expression is a binary operation with multiplication + if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { + let mut curr_expr = left_expr; + + // Traverse the left-hand expressions + loop { + match &curr_expr.kind { + // If the expression is a binary operation, inspect its kind + ExprKind::Binary(inner_expr, binop, _) => match binop.kind { + // Found division, push the span and stop traversal + BinOpKind::Div => { + self.items.push(curr_expr.span); + break; + } + // Continue traversing if it's another multiplication + BinOpKind::Mul => { + curr_expr = inner_expr; + } + // Stop if it's any other binary operator + _ => { + break; + } + }, + + // Handle tuple expressions that contain one nested expression + ExprKind::Tuple([Some(inner_expr)]) => { + curr_expr = inner_expr; + } + + // Stop for any other kind of expression + _ => { + break; + } + } + } + } } } +#[cfg(test)] mod test { - use std::{path::Path, vec}; - - use solar_ast::ast; - use solar_interface::{sym::assert, ColorChoice, Session}; + use solar_ast::{ast, visit::Visit}; + use solar_interface::{ColorChoice, Session}; + use std::path::Path; - use crate::ForgeLint; + use crate::DivideBeforeMultiply; - #[allow(unused)] - use super::*; - #[allow(unused)] #[test] fn test_divide_before_multiply() -> eyre::Result<()> { let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); From 68dad345ca53892bb1b8f7e24d98964119bd05e6 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 01:20:33 -0500 Subject: [PATCH 010/107] wip --- crates/lint/src/vulnerability.rs | 63 ++++++++++--------- crates/lint/testdata/DivideBeforeMultiply.sol | 32 ++++++++++ 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/crates/lint/src/vulnerability.rs b/crates/lint/src/vulnerability.rs index 5acb127554cae..4b780b0f75245 100644 --- a/crates/lint/src/vulnerability.rs +++ b/crates/lint/src/vulnerability.rs @@ -4,44 +4,45 @@ use solar_ast::{ }; use crate::DivideBeforeMultiply; + impl<'ast> Visit<'ast> for DivideBeforeMultiply { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // Check if the current expression is a binary operation with multiplication - if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { - let mut curr_expr = left_expr; - - // Traverse the left-hand expressions - loop { - match &curr_expr.kind { - // If the expression is a binary operation, inspect its kind - ExprKind::Binary(inner_expr, binop, _) => match binop.kind { - // Found division, push the span and stop traversal - BinOpKind::Div => { - self.items.push(curr_expr.span); - break; - } - // Continue traversing if it's another multiplication - BinOpKind::Mul => { - curr_expr = inner_expr; - } - // Stop if it's any other binary operator - _ => { - break; - } - }, - - // Handle tuple expressions that contain one nested expression - ExprKind::Tuple([Some(inner_expr)]) => { - curr_expr = inner_expr; - } + match &expr.kind { + ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, right_expr) => { + if contains_division(left_expr) { + self.items.push(expr.span); + } + } - // Stop for any other kind of expression - _ => { - break; + ExprKind::Tuple(inner_exprs) => { + for opt_expr in inner_exprs.iter() { + if let Some(inner_expr) = opt_expr { + self.visit_expr(inner_expr); } } } + + _ => {} } + + self.walk_expr(expr); + } +} + +fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { + match &expr.kind { + ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, + ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) => { + contains_division(left_expr) + } + ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { + if let Some(inner_expr) = opt_expr { + contains_division(inner_expr) + } else { + false + } + }), + _ => false, } } diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol index 3382e49189ccb..ad970c4ba4cf1 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.sol +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -1,5 +1,37 @@ contract Contract0 { function arithmetic() public { (1 / 2) * 3; // Unsafe + (1 * 2) / 3; // Safe + (1 / 2) * 3; // Unsafe + (1 * 2) / 3; // Safe + ((1 / 2) * 3) * 4; // Unsafe + ((1 * 2) / 3) * 4; // Unsafe + (1 / 2 / 3) * 4; // Unsafe + (1 / (2 + 3)) * 4; // Unsafe + (1 / 2 + 3) * 4; // Safe + (1 / 2 - 3) * 4; // Safe + (1 + 2 / 3) * 4; // Safe + (1 / 2 - 3) * 4; // Safe + ((1 / 2) % 3) * 4; // Safe + 1 / (2 * 3 + 3); // Safe + 1 / ((2 / 3) * 3); // Unsafe + 1 / ((2 * 3) + 3); // Safe + + // uint256 x = 5; + // x /= 2 * 3; // Unsafe + // x /= (2 * 3); // Unsafe + // x /= 2 * 3 - 4; // Unsafe + // x /= (2 * 3) % 4; // Unsafe + // x /= (2 * 3) | 4; // Unsafe + // x /= (2 * 3) & 4; // Unsafe + // x /= (2 * 3) ^ 4; // Unsafe + // x /= (2 * 3) << 4; // Unsafe + // x /= (2 * 3) >> 4; // Unsafe + // x /= 3 % 4; // Safe + // x /= 3 | 4; // Safe + // x /= 3 & 4; // Safe + // x /= 3 ^ 4; // Safe + // x /= 3 << 4; // Safe + // x /= 3 >> 4; // Safe } } From da7038d80f3297121916a7e5c42eda52e1c42316 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 01:26:00 -0500 Subject: [PATCH 011/107] wip --- crates/lint/src/lib.rs | 2 +- crates/lint/src/qa.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 03d67b93ddb43..f1d8e77cf6174 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -125,6 +125,6 @@ declare_lints!( // QA VariableCamelCase, VariableCapsCase, - VariablePascalCase, + StructPascalCase, FunctionCamelCase, ); diff --git a/crates/lint/src/qa.rs b/crates/lint/src/qa.rs index 65a789e2bb23b..310fa38535655 100644 --- a/crates/lint/src/qa.rs +++ b/crates/lint/src/qa.rs @@ -5,7 +5,7 @@ use solar_ast::{ visit::Visit, }; -use crate::{FunctionCamelCase, VariableCamelCase, VariableCapsCase, VariablePascalCase}; +use crate::{FunctionCamelCase, StructPascalCase, VariableCamelCase, VariableCapsCase}; impl<'ast> Visit<'ast> for VariableCamelCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { @@ -37,7 +37,7 @@ impl<'ast> Visit<'ast> for VariableCapsCase { } } -impl<'ast> Visit<'ast> for VariablePascalCase { +impl<'ast> Visit<'ast> for StructPascalCase { fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { if !is_pascal_case(strukt.name.as_str()) { self.items.push(strukt.name.span); From 11b638d65d0a05fbc6d75c5361a2c7bccd87c3b6 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 01:33:54 -0500 Subject: [PATCH 012/107] wip --- crates/lint/src/vulnerability.rs | 43 +++++++------------ crates/lint/testdata/DivideBeforeMultiply.sol | 6 +-- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/crates/lint/src/vulnerability.rs b/crates/lint/src/vulnerability.rs index 4b780b0f75245..3160b2a0c6f13 100644 --- a/crates/lint/src/vulnerability.rs +++ b/crates/lint/src/vulnerability.rs @@ -7,42 +7,29 @@ use crate::DivideBeforeMultiply; impl<'ast> Visit<'ast> for DivideBeforeMultiply { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - match &expr.kind { - ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, right_expr) => { - if contains_division(left_expr) { - self.items.push(expr.span); - } - } - - ExprKind::Tuple(inner_exprs) => { - for opt_expr in inner_exprs.iter() { - if let Some(inner_expr) = opt_expr { - self.visit_expr(inner_expr); - } - } + if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { + if self.contains_division(left_expr) { + self.items.push(expr.span); } - - _ => {} } self.walk_expr(expr); } } -fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { - match &expr.kind { - ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, - ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) => { - contains_division(left_expr) +impl DivideBeforeMultiply { + fn contains_division<'ast>(&self, expr: &'ast Expr<'ast>) -> bool { + match &expr.kind { + ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, + ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { + if let Some(inner_expr) = opt_expr { + self.contains_division(inner_expr) + } else { + false + } + }), + _ => false, } - ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { - if let Some(inner_expr) = opt_expr { - contains_division(inner_expr) - } else { - false - } - }), - _ => false, } } diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol index ad970c4ba4cf1..4cb86c73686a2 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.sol +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -1,7 +1,5 @@ contract Contract0 { function arithmetic() public { - (1 / 2) * 3; // Unsafe - (1 * 2) / 3; // Safe (1 / 2) * 3; // Unsafe (1 * 2) / 3; // Safe ((1 / 2) * 3) * 4; // Unsafe @@ -17,8 +15,8 @@ contract Contract0 { 1 / ((2 / 3) * 3); // Unsafe 1 / ((2 * 3) + 3); // Safe - // uint256 x = 5; - // x /= 2 * 3; // Unsafe + uint256 x = 5; + x /= 2 * 3; // Unsafe // x /= (2 * 3); // Unsafe // x /= 2 * 3 - 4; // Unsafe // x /= (2 * 3) % 4; // Unsafe From 72e3b3acc45db26623612e4f95400bb636dc920b Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 01:42:00 -0500 Subject: [PATCH 013/107] fix div before mul --- crates/lint/src/optimization.rs | 7 ++----- crates/lint/src/vulnerability.rs | 9 +++------ crates/lint/testdata/DivideBeforeMultiply.sol | 17 ----------------- 3 files changed, 5 insertions(+), 28 deletions(-) diff --git a/crates/lint/src/optimization.rs b/crates/lint/src/optimization.rs index 23f5140ffcab1..d3d8256331276 100644 --- a/crates/lint/src/optimization.rs +++ b/crates/lint/src/optimization.rs @@ -34,13 +34,10 @@ mod test { let arena = ast::Arena::new(); let mut parser = - solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol")) - .expect("TODO:"); + solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol"))?; // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); - - dbg!(&ast); + let ast = parser.parse_file().map_err(|e| e.emit())?; let mut pattern = Keccak256::default(); pattern.visit_source_unit(&ast); diff --git a/crates/lint/src/vulnerability.rs b/crates/lint/src/vulnerability.rs index 3160b2a0c6f13..f5692bbaa53ac 100644 --- a/crates/lint/src/vulnerability.rs +++ b/crates/lint/src/vulnerability.rs @@ -52,18 +52,15 @@ mod test { &sess, &arena, Path::new("testdata/DivideBeforeMultiply.sol"), - ) - .expect("TODO:"); + )?; // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); - - dbg!(&ast); + let ast = parser.parse_file().map_err(|e| e.emit())?; let mut pattern = DivideBeforeMultiply::default(); pattern.visit_source_unit(&ast); - assert_eq!(pattern.items.len(), 0); + assert_eq!(pattern.items.len(), 6); Ok(()) }); diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol index 4cb86c73686a2..8e3e9525f7ba1 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.sol +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -14,22 +14,5 @@ contract Contract0 { 1 / (2 * 3 + 3); // Safe 1 / ((2 / 3) * 3); // Unsafe 1 / ((2 * 3) + 3); // Safe - - uint256 x = 5; - x /= 2 * 3; // Unsafe - // x /= (2 * 3); // Unsafe - // x /= 2 * 3 - 4; // Unsafe - // x /= (2 * 3) % 4; // Unsafe - // x /= (2 * 3) | 4; // Unsafe - // x /= (2 * 3) & 4; // Unsafe - // x /= (2 * 3) ^ 4; // Unsafe - // x /= (2 * 3) << 4; // Unsafe - // x /= (2 * 3) >> 4; // Unsafe - // x /= 3 % 4; // Safe - // x /= 3 | 4; // Safe - // x /= 3 & 4; // Safe - // x /= 3 ^ 4; // Safe - // x /= 3 << 4; // Safe - // x /= 3 >> 4; // Safe } } From dd56c01215e51c24aa94e0a6e9b9ae84ff2c2855 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 22:45:50 -0500 Subject: [PATCH 014/107] update lint args --- crates/forge/bin/cmd/lint.rs | 130 ++++++++++++++++++++++------------- crates/lint/src/lib.rs | 4 +- 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index c98ec8d443051..37db736ba7021 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,6 +1,7 @@ +use clap::ValueEnum; use clap::{Parser, ValueHint}; use eyre::Result; -use forge_lint::{ForgeLint, Input}; +use forge_lint::{Input, Linter}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; @@ -10,19 +11,56 @@ use std::{ path::{Path, PathBuf}, }; +#[derive(Clone, Debug, ValueEnum)] +pub enum OutputFormat { + Json, + Markdown, +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum Severity { + High, + Med, + Low, + Info, + Gas, +} + /// CLI arguments for `forge fmt`. #[derive(Clone, Debug, Parser)] pub struct LintArgs { - /// Path to the file, directory or '-' to read from stdin. - #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))] - paths: Vec, - /// 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, + + /// Include only the specified files. + #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] + include: Option>, + + /// Exclude the specified files. + #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] + exclude: Option>, + + /// Format of the output. + /// + /// Supported values: `json` or `markdown`. + #[arg(long, value_name = "FORMAT", default_value = "json")] + format: OutputFormat, + + /// Use only selected severities for output. + /// + /// Supported values: `high`, `med`, `low`, `info`, `gas`. + #[arg(long, value_name = "SEVERITY", num_args(1..))] + severity: Option>, + + /// Show descriptions in the output. + /// + /// Disabled by default to avoid long console output. + #[arg(long)] + with_description: bool, } impl_figment_convert_basic!(LintArgs); @@ -39,47 +77,47 @@ impl LintArgs { let cwd = std::env::current_dir()?; - let input = match &self.paths[..] { - [] => { - // Retrieve the project paths, and filter out the ignored ones. - let project_paths: Vec = config - .project_paths::() - .input_files_iter() - .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p)))) - .collect(); - Input::Paths(project_paths) - } - [one] if one == Path::new("-") => { - let mut s = String::new(); - io::stdin().read_to_string(&mut s).expect("Failed to read from stdin"); - Input::Stdin(s) - } - paths => { - let mut inputs = Vec::with_capacity(paths.len()); - for path in paths { - if !ignored.is_empty() - && ((path.is_absolute() && ignored.contains(path)) - || ignored.contains(&cwd.join(path))) - { - continue; - } - - if path.is_dir() { - inputs.extend(foundry_compilers::utils::source_files_iter( - path, - SOLC_EXTENSIONS, - )); - } else if path.is_sol() { - inputs.push(path.to_path_buf()); - } else { - warn!("Cannot process path {}", path.display()); - } - } - Input::Paths(inputs) - } - }; - - ForgeLint::new(input).lint(); + // let input = match &self.paths[..] { + // [] => { + // // Retrieve the project paths, and filter out the ignored ones. + // let project_paths: Vec = config + // .project_paths::() + // .input_files_iter() + // .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p)))) + // .collect(); + // Input::Paths(project_paths) + // } + // [one] if one == Path::new("-") => { + // let mut s = String::new(); + // io::stdin().read_to_string(&mut s).expect("Failed to read from stdin"); + // Input::Stdin(s) + // } + // paths => { + // let mut inputs = Vec::with_capacity(paths.len()); + // for path in paths { + // if !ignored.is_empty() + // && ((path.is_absolute() && ignored.contains(path)) + // || ignored.contains(&cwd.join(path))) + // { + // continue; + // } + + // if path.is_dir() { + // inputs.extend(foundry_compilers::utils::source_files_iter( + // path, + // SOLC_EXTENSIONS, + // )); + // } else if path.is_sol() { + // inputs.push(path.to_path_buf()); + // } else { + // warn!("Cannot process path {}", path.display()); + // } + // } + // Input::Paths(inputs) + // } + // }; + + // Linter::new(input).lint(); Ok(()) } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index f1d8e77cf6174..7e1162765e184 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -16,12 +16,12 @@ pub enum Input { Paths(Vec), } -pub struct ForgeLint { +pub struct Linter { pub input: Input, pub lints: Vec, } -impl ForgeLint { +impl Linter { // TODO: Add config specifying which lints to run pub fn new(input: Input) -> Self { Self { input, lints: Lint::all() } From aabd8ce4536c6643511c1db775450f2ae7e6e74f Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 23:12:51 -0500 Subject: [PATCH 015/107] wip --- crates/forge/bin/cmd/lint.rs | 79 +++++++---------------------------- crates/lint/src/lib.rs | 81 +++++++++++++++++------------------- 2 files changed, 53 insertions(+), 107 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 37db736ba7021..0ea82c7059945 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,32 +1,18 @@ use clap::ValueEnum; use clap::{Parser, ValueHint}; use eyre::Result; -use forge_lint::{Input, Linter}; +use forge_lint::Linter; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; +use std::collections::HashSet; use std::{ io, io::Read, path::{Path, PathBuf}, }; -#[derive(Clone, Debug, ValueEnum)] -pub enum OutputFormat { - Json, - Markdown, -} - -#[derive(Clone, Debug, ValueEnum)] -pub enum Severity { - High, - Med, - Low, - Info, - Gas, -} - -/// CLI arguments for `forge fmt`. +/// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] pub struct LintArgs { /// The project's root path. @@ -50,6 +36,7 @@ pub struct LintArgs { #[arg(long, value_name = "FORMAT", default_value = "json")] format: OutputFormat, + // TODO: output file /// Use only selected severities for output. /// /// Supported values: `high`, `med`, `low`, `info`, `gas`. @@ -68,56 +55,20 @@ impl_figment_convert_basic!(LintArgs); impl LintArgs { pub fn run(self) -> Result<()> { let config = self.try_load_config_emit_warnings()?; + let root = self.root.unwrap_or_else(|| std::env::current_dir().unwrap()); - // Expand ignore globs and canonicalize from the get go - let ignored = expand_globs(&config.root, config.fmt.ignore.iter())? - .iter() - .flat_map(foundry_common::fs::canonicalize_path) - .collect::>(); - - let cwd = std::env::current_dir()?; - - // let input = match &self.paths[..] { - // [] => { - // // Retrieve the project paths, and filter out the ignored ones. - // let project_paths: Vec = config - // .project_paths::() - // .input_files_iter() - // .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p)))) - // .collect(); - // Input::Paths(project_paths) - // } - // [one] if one == Path::new("-") => { - // let mut s = String::new(); - // io::stdin().read_to_string(&mut s).expect("Failed to read from stdin"); - // Input::Stdin(s) - // } - // paths => { - // let mut inputs = Vec::with_capacity(paths.len()); - // for path in paths { - // if !ignored.is_empty() - // && ((path.is_absolute() && ignored.contains(path)) - // || ignored.contains(&cwd.join(path))) - // { - // continue; - // } + let mut paths: Vec = if let Some(include_paths) = &self.include { + include_paths.iter().filter(|path| path.exists()).cloned().collect() + } else { + foundry_compilers::utils::source_files_iter(&root, &[".sol"]).collect() + }; - // if path.is_dir() { - // inputs.extend(foundry_compilers::utils::source_files_iter( - // path, - // SOLC_EXTENSIONS, - // )); - // } else if path.is_sol() { - // inputs.push(path.to_path_buf()); - // } else { - // warn!("Cannot process path {}", path.display()); - // } - // } - // Input::Paths(inputs) - // } - // }; + if let Some(exclude_paths) = &self.exclude { + let exclude_set = exclude_paths.iter().collect::>(); + paths.retain(|path| !exclude_set.contains(path)); + } - // Linter::new(input).lint(); + Linter::new(paths).lint(); Ok(()) } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 7e1162765e184..2f91447d1e953 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -10,64 +10,59 @@ use solar_ast::{ visit::Visit, }; -#[derive(Debug)] -pub enum Input { - Stdin(String), - Paths(Vec), +#[derive(Clone, Debug)] +pub enum OutputFormat { + Json, + Markdown, +} + +#[derive(Clone, Debug)] +pub enum Severity { + High, + Med, + Low, + Info, + Gas, } pub struct Linter { - pub input: Input, + pub input: Vec, pub lints: Vec, } impl Linter { // TODO: Add config specifying which lints to run - pub fn new(input: Input) -> Self { + pub fn new(input: Vec) -> Self { Self { input, lints: Lint::all() } } + pub fn with_severity(self, severity: Vec) -> Self { + todo!() + } + pub fn lint(self) { - match self.input { - Input::Stdin(source) => { - // Create a new session with a buffer emitter. - // This is required to capture the emitted diagnostics and to return them at the - // end. - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - // Enter the context and parse the file. - let _ = sess.enter(|| -> solar_interface::Result<()> { - // Set up the parser. - let arena = ast::Arena::new(); - - let mut parser = - solar_parse::Parser::from_file(&sess, &arena, &Path::new(&source)) - .expect("TODO:"); - - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); - - for mut lint in self.lints { - lint.visit_source_unit(&ast); - } - - Ok(()) - }); - } + // Create a new session with a buffer emitter. + // This is required to capture the emitted diagnostics and to return them at the + // end. + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - Input::Paths(paths) => { - if paths.is_empty() { - // sh_warn!( - // "Nothing to lint.\n\ - // HINT: If you are working outside of the project, \ - // try providing paths to your source files: `forge fmt `" - // )?; - todo!(); - } + // Enter the context and parse the file. + let _ = sess.enter(|| -> solar_interface::Result<()> { + // Set up the parser. + let arena = ast::Arena::new(); + + let mut parser = + solar_parse::Parser::from_file(&sess, &arena, &Path::new(&source)).expect("TODO:"); - todo!(); + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); + + for mut lint in self.lints { + lint.visit_source_unit(&ast); } - }; + + Ok(()) + }); } } From f0dc57bedf8e49272c4aa5006fe99cbdf0a474c2 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 23:27:10 -0500 Subject: [PATCH 016/107] update declare lints macro --- crates/lint/src/{optimization.rs => gas.rs} | 0 crates/lint/src/{qa.rs => info.rs} | 0 crates/lint/src/lib.rs | 70 +++++++++++++++----- crates/lint/src/{vulnerability.rs => med.rs} | 0 4 files changed, 53 insertions(+), 17 deletions(-) rename crates/lint/src/{optimization.rs => gas.rs} (100%) rename crates/lint/src/{qa.rs => info.rs} (100%) rename crates/lint/src/{vulnerability.rs => med.rs} (100%) diff --git a/crates/lint/src/optimization.rs b/crates/lint/src/gas.rs similarity index 100% rename from crates/lint/src/optimization.rs rename to crates/lint/src/gas.rs diff --git a/crates/lint/src/qa.rs b/crates/lint/src/info.rs similarity index 100% rename from crates/lint/src/qa.rs rename to crates/lint/src/info.rs diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 2f91447d1e953..10b141286c658 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,6 +1,6 @@ -pub mod optimization; -pub mod qa; -pub mod vulnerability; +pub mod gas; +pub mod info; +pub mod med; use std::path::{Path, PathBuf}; @@ -36,8 +36,14 @@ impl Linter { Self { input, lints: Lint::all() } } - pub fn with_severity(self, severity: Vec) -> Self { - todo!() + pub fn with_severity(self, severity: Option>) -> Self { + if let Some(severity) = severity { + for lint in self.lints { + //TODO: remove if lint sev is not in list + } + } + + self } pub fn lint(self) { @@ -67,7 +73,7 @@ impl Linter { } macro_rules! declare_lints { - ($($name:ident),* $(,)?) => { + ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { #[derive(Debug)] pub enum Lint { $( @@ -75,8 +81,8 @@ macro_rules! declare_lints { )* } - impl Lint { + /// Returns all available lints as a vector pub fn all() -> Vec { vec![ $( @@ -84,6 +90,20 @@ macro_rules! declare_lints { )* ] } + + /// Returns the metadata for all lints + pub fn metadata() -> Vec<(String, Severity, String, String)> { + vec![ + $( + ( + stringify!($name).to_string(), // Struct name + $severity, // Severity + $lint_name.to_string(), // Lint name + $description.to_string(), // Description + ), + )* + ] + } } impl<'ast> Visit<'ast> for Lint { @@ -106,20 +126,36 @@ macro_rules! declare_lints { pub fn new() -> Self { Self { items: Vec::new() } } + + /// Returns the severity of the lint + pub fn severity() -> Severity { + $severity + } + + /// Returns the name of the lint + pub fn lint_name() -> &'static str { + $lint_name + } + + /// Returns the description of the lint + pub fn description() -> &'static str { + $description + } } )* }; } -// TODO: Group by opts, vulns, qa, add description for each lint declare_lints!( - //Optimizations - Keccak256, - // Vunlerabilities - DivideBeforeMultiply, - // QA - VariableCamelCase, - VariableCapsCase, - StructPascalCase, - FunctionCamelCase, + // Gas Optimizations + (Keccak256, Severity::Gas, "Keccak256", "TODO:"), + //High + // Med + (DivideBeforeMultiply, Severity::Med, "Divide Before Multiply", "TODO:"), + // Low + // Info + (VariableCamelCase, Severity::Info, "Variable Camel Case", "TODO:"), + (VariableCapsCase, Severity::Info, "Variable Caps Case", "TODO:"), + (StructPascalCase, Severity::Info, "Struct Pascal Case", "TODO:"), + (FunctionCamelCase, Severity::Info, "Function Camel Case", "TODO:") ); diff --git a/crates/lint/src/vulnerability.rs b/crates/lint/src/med.rs similarity index 100% rename from crates/lint/src/vulnerability.rs rename to crates/lint/src/med.rs From 3417d4984989bd893200039377c09dc909cf085c Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 23:30:39 -0500 Subject: [PATCH 017/107] update with_severity --- crates/lint/src/lib.rs | 45 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 10b141286c658..7aafa0e9d3107 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -16,7 +16,7 @@ pub enum OutputFormat { Markdown, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Severity { High, Med, @@ -31,18 +31,14 @@ pub struct Linter { } impl Linter { - // TODO: Add config specifying which lints to run pub fn new(input: Vec) -> Self { Self { input, lints: Lint::all() } } - pub fn with_severity(self, severity: Option>) -> Self { - if let Some(severity) = severity { - for lint in self.lints { - //TODO: remove if lint sev is not in list - } + pub fn with_severity(mut self, severities: Option>) -> Self { + if let Some(severities) = severities { + self.lints.retain(|lint| severities.contains(&lint.severity())); } - self } @@ -82,7 +78,6 @@ macro_rules! declare_lints { } impl Lint { - /// Returns all available lints as a vector pub fn all() -> Vec { vec![ $( @@ -91,18 +86,28 @@ macro_rules! declare_lints { ] } - /// Returns the metadata for all lints - pub fn metadata() -> Vec<(String, Severity, String, String)> { - vec![ + pub fn severity(&self) -> Severity { + match self { $( - ( - stringify!($name).to_string(), // Struct name - $severity, // Severity - $lint_name.to_string(), // Lint name - $description.to_string(), // Description - ), + Lint::$name(_) => $severity, )* - ] + } + } + + pub fn name(&self) -> &'static str { + match self { + $( + Lint::$name(_) => $lint_name, + )* + } + } + + pub fn description(&self) -> &'static str { + match self { + $( + Lint::$name(_) => $description, + )* + } } } @@ -133,7 +138,7 @@ macro_rules! declare_lints { } /// Returns the name of the lint - pub fn lint_name() -> &'static str { + pub fn name() -> &'static str { $lint_name } From 7e0547247ecdc9150a1b644ccbbef8e318627ce6 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Mon, 23 Dec 2024 23:32:56 -0500 Subject: [PATCH 018/107] configure linter --- crates/forge/bin/cmd/lint.rs | 5 ++++- crates/lint/src/lib.rs | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 0ea82c7059945..e7cb76781bc22 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -68,7 +68,10 @@ impl LintArgs { paths.retain(|path| !exclude_set.contains(path)); } - Linter::new(paths).lint(); + Linter::new(paths) + .with_severity(self.severity) + .with_description(self.with_description) + .lint(); Ok(()) } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 7aafa0e9d3107..a63b851151526 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -28,20 +28,26 @@ pub enum Severity { pub struct Linter { pub input: Vec, pub lints: Vec, + pub description: bool, } impl Linter { pub fn new(input: Vec) -> Self { - Self { input, lints: Lint::all() } + Self { input, lints: Lint::all(), description: false } } - pub fn with_severity(mut self, severities: Option>) -> Self { - if let Some(severities) = severities { - self.lints.retain(|lint| severities.contains(&lint.severity())); + pub fn with_severity(mut self, severity: Option>) -> Self { + if let Some(severity) = severity { + self.lints.retain(|lint| severity.contains(&lint.severity())); } self } + pub fn with_description(mut self, description: bool) -> Self { + self.description = description; + self + } + pub fn lint(self) { // Create a new session with a buffer emitter. // This is required to capture the emitted diagnostics and to return them at the From 0d56fe03eb00400d07c44a37c115846ee821722d Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 24 Dec 2024 00:11:59 -0500 Subject: [PATCH 019/107] wip --- crates/forge/bin/cmd/lint.rs | 3 ++- crates/lint/src/lib.rs | 42 ++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index e7cb76781bc22..f129d5a9e7c3a 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,8 +1,9 @@ use clap::ValueEnum; use clap::{Parser, ValueHint}; use eyre::Result; -use forge_lint::Linter; +use forge_lint::{Linter, Severity}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; +use foundry_common::shell::OutputFormat; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; use std::collections::HashSet; diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index a63b851151526..e1f6924a6718f 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -2,7 +2,10 @@ pub mod gas; pub mod info; pub mod med; -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use solar_ast::{ ast::{self, SourceUnit, Span}, @@ -54,21 +57,29 @@ impl Linter { // end. let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + let mut findings = HashMap::new(); + // Enter the context and parse the file. let _ = sess.enter(|| -> solar_interface::Result<()> { // Set up the parser. let arena = ast::Arena::new(); - let mut parser = - solar_parse::Parser::from_file(&sess, &arena, &Path::new(&source)).expect("TODO:"); + for file in &self.input { + let lints = self.lints.clone(); - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit()).expect("TODO:"); + let mut parser = solar_parse::Parser::from_file(&sess, &arena, file) + .expect("Failed to create parser"); + let ast = parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - for mut lint in self.lints { - lint.visit_source_unit(&ast); + // Run all lints on the parsed AST + for mut lint in lints { + let results = lint.lint(&ast); + findings.entry(lint.clone()).or_insert_with(Vec::new).extend(results); + } } + // TODO: Output the findings + Ok(()) }); } @@ -76,7 +87,7 @@ impl Linter { macro_rules! declare_lints { ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { - #[derive(Debug)] + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Lint { $( $name($name), @@ -115,6 +126,19 @@ macro_rules! declare_lints { )* } } + + + /// Lint a source unit and return the findings + pub fn lint<'ast>(&mut self, source_unit: &SourceUnit<'ast>) -> Vec { + match self { + $( + Lint::$name(lint) => { + lint.visit_source_unit(source_unit); + lint.items.clone() + }, + )* + } + } } impl<'ast> Visit<'ast> for Lint { @@ -128,7 +152,7 @@ macro_rules! declare_lints { } $( - #[derive(Debug, Default)] + #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct $name { pub items: Vec, } From 28b6f869f710330ab6919ba99ec91d043f53c064 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 24 Dec 2024 00:29:06 -0500 Subject: [PATCH 020/107] update hash value --- crates/lint/src/gas.rs | 8 ++++---- crates/lint/src/lib.rs | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/lint/src/gas.rs b/crates/lint/src/gas.rs index d3d8256331276..ac9b1462a0cc7 100644 --- a/crates/lint/src/gas.rs +++ b/crates/lint/src/gas.rs @@ -3,9 +3,9 @@ use solar_ast::{ visit::Visit, }; -use crate::Keccak256; +use crate::AsmKeccak256; -impl<'ast> Visit<'ast> for Keccak256 { +impl<'ast> Visit<'ast> for AsmKeccak256 { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { @@ -24,7 +24,7 @@ mod test { use solar_interface::{ColorChoice, Session}; use std::path::Path; - use crate::Keccak256; + use crate::AsmKeccak256; #[test] fn test_keccak256() -> eyre::Result<()> { @@ -39,7 +39,7 @@ mod test { // Parse the file. let ast = parser.parse_file().map_err(|e| e.emit())?; - let mut pattern = Keccak256::default(); + let mut pattern = AsmKeccak256::default(); pattern.visit_source_unit(&ast); assert_eq!(pattern.items.len(), 2); diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index e1f6924a6718f..758a7a9fe9a28 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -4,6 +4,7 @@ pub mod med; use std::{ collections::HashMap, + hash::Hasher, path::{Path, PathBuf}, }; @@ -87,7 +88,7 @@ impl Linter { macro_rules! declare_lints { ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { - #[derive(Debug, Clone, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Lint { $( $name($name), @@ -151,6 +152,13 @@ macro_rules! declare_lints { } } + + impl std::hash::Hash for Lint { + fn hash(&self, state: &mut H) { + self.name().hash(state); + } + } + $( #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct $name { @@ -183,14 +191,14 @@ macro_rules! declare_lints { declare_lints!( // Gas Optimizations - (Keccak256, Severity::Gas, "Keccak256", "TODO:"), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO:"), //High // Med - (DivideBeforeMultiply, Severity::Med, "Divide Before Multiply", "TODO:"), + (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO:"), // Low // Info - (VariableCamelCase, Severity::Info, "Variable Camel Case", "TODO:"), - (VariableCapsCase, Severity::Info, "Variable Caps Case", "TODO:"), - (StructPascalCase, Severity::Info, "Struct Pascal Case", "TODO:"), - (FunctionCamelCase, Severity::Info, "Function Camel Case", "TODO:") + (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO:"), + (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO:"), + (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO:"), + (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO:") ); From d89b62a451d9c6668853f343611e641faf389a7f Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 24 Dec 2024 01:25:32 -0500 Subject: [PATCH 021/107] fix read in source --- Cargo.lock | 1 + crates/forge/bin/cmd/lint.rs | 64 +++++++++++++++++++++++------------- crates/lint/Cargo.toml | 1 + crates/lint/src/lib.rs | 28 +++++++++++----- 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03eba681e1e55..3f12caf58d99e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3558,6 +3558,7 @@ dependencies = [ name = "forge-lint" version = "0.3.0" dependencies = [ + "clap", "eyre", "regex", "solar-ast", diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index f129d5a9e7c3a..7ca9d97113f13 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,17 +1,13 @@ -use clap::ValueEnum; use clap::{Parser, ValueHint}; -use eyre::Result; -use forge_lint::{Linter, Severity}; -use foundry_cli::utils::{FoundryPathExt, LoadConfig}; -use foundry_common::shell::OutputFormat; -use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; -use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; +use eyre::{bail, Result}; +use forge_lint::{Linter, OutputFormat, Severity}; +use foundry_cli::utils::LoadConfig; +use foundry_compilers::utils::{source_files_iter, SOLC_EXTENSIONS}; +use foundry_config::filter::expand_globs; +use foundry_config::impl_figment_convert_basic; use std::collections::HashSet; -use std::{ - io, - io::Read, - path::{Path, PathBuf}, -}; +use std::fs; +use std::path::PathBuf; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] @@ -23,22 +19,22 @@ pub struct LintArgs { #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")] root: Option, - /// Include only the specified files. + /// Include only the specified files when linting. #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] include: Option>, - /// Exclude the specified files. + /// Exclude the specified files when linting. #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] exclude: Option>, + // TODO: support writing to output file /// Format of the output. /// /// Supported values: `json` or `markdown`. #[arg(long, value_name = "FORMAT", default_value = "json")] format: OutputFormat, - // TODO: output file - /// Use only selected severities for output. + /// Specifies which lints to run based on severity. /// /// Supported values: `high`, `med`, `low`, `info`, `gas`. #[arg(long, value_name = "SEVERITY", num_args(1..))] @@ -56,20 +52,42 @@ impl_figment_convert_basic!(LintArgs); impl LintArgs { pub fn run(self) -> Result<()> { let config = self.try_load_config_emit_warnings()?; - let root = self.root.unwrap_or_else(|| std::env::current_dir().unwrap()); + let root = if let Some(root) = &self.root { root } else { &config.root }; - let mut paths: Vec = if let Some(include_paths) = &self.include { + // Expand ignore globs and canonicalize paths + let mut ignored = expand_globs(&root, config.fmt.ignore.iter())? + .iter() + .flat_map(foundry_common::fs::canonicalize_path) + .collect::>(); + + // Add explicitly excluded paths to the ignored set + if let Some(exclude_paths) = &self.exclude { + ignored.extend(exclude_paths.iter().flat_map(foundry_common::fs::canonicalize_path)); + } + + let entries = fs::read_dir(root).unwrap(); + println!("Files in directory: {}", root.display()); + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + println!("{}", path.display()); + } + + let mut input: Vec = if let Some(include_paths) = &self.include { include_paths.iter().filter(|path| path.exists()).cloned().collect() } else { - foundry_compilers::utils::source_files_iter(&root, &[".sol"]).collect() + source_files_iter(&root, SOLC_EXTENSIONS) + .filter(|p| !(ignored.contains(p) || ignored.contains(&root.join(p)))) + .collect() }; - if let Some(exclude_paths) = &self.exclude { - let exclude_set = exclude_paths.iter().collect::>(); - paths.retain(|path| !exclude_set.contains(path)); + input.retain(|path| !ignored.contains(path)); + + if input.is_empty() { + bail!("No source files found in path"); } - Linter::new(paths) + Linter::new(input) .with_severity(self.severity) .with_description(self.with_description) .lint(); diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 54154d5eb9a46..8c6407a2d33bf 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -19,3 +19,4 @@ solar-ast.workspace = true solar-interface.workspace = true eyre.workspace = true regex = "1.11" +clap = { version = "4", features = ["derive"] } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 758a7a9fe9a28..b0385bbb86b06 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -8,19 +8,20 @@ use std::{ path::{Path, PathBuf}, }; +use clap::ValueEnum; use solar_ast::{ ast::{self, SourceUnit, Span}, interface::{ColorChoice, Session}, visit::Visit, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ValueEnum)] pub enum OutputFormat { Json, Markdown, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum Severity { High, Med, @@ -79,7 +80,16 @@ impl Linter { } } - // TODO: Output the findings + // TODO: make the output nicer + for finding in findings { + let (lint, results) = finding; + let description = if self.description { lint.description() } else { "" }; + + println!("{}: {}", lint.name(), description); + for result in results { + println!(" - {:?}", result); + } + } Ok(()) }); @@ -191,14 +201,14 @@ macro_rules! declare_lints { declare_lints!( // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO:"), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), //High // Med - (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO:"), + (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), // Low // Info - (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO:"), - (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO:"), - (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO:"), - (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO:") + (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), + (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), + (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), + (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description") ); From a68f385e2a2e9ca565159d40a6e28d8ec802d5af Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 24 Dec 2024 01:40:40 -0500 Subject: [PATCH 022/107] rayon --- Cargo.lock | 1 + crates/lint/Cargo.toml | 2 ++ crates/lint/src/lib.rs | 76 +++++++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f12caf58d99e..1a0dc19a078e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3560,6 +3560,7 @@ version = "0.3.0" dependencies = [ "clap", "eyre", + "rayon", "regex", "solar-ast", "solar-interface", diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 8c6407a2d33bf..a5a6ab72f8352 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -18,5 +18,7 @@ solar-parse.workspace = true solar-ast.workspace = true solar-interface.workspace = true eyre.workspace = true +rayon.workspace = true + regex = "1.11" clap = { version = "4", features = ["derive"] } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index b0385bbb86b06..c356c12e1807a 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -2,11 +2,8 @@ pub mod gas; pub mod info; pub mod med; -use std::{ - collections::HashMap, - hash::Hasher, - path::{Path, PathBuf}, -}; +use rayon::prelude::*; +use std::{collections::HashMap, hash::Hasher, path::PathBuf}; use clap::ValueEnum; use solar_ast::{ @@ -54,45 +51,54 @@ impl Linter { } pub fn lint(self) { - // Create a new session with a buffer emitter. - // This is required to capture the emitted diagnostics and to return them at the - // end. - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + let all_findings = self + .input + .par_iter() + .map(|file| { + let lints = self.lints.clone(); + let mut local_findings = HashMap::new(); - let mut findings = HashMap::new(); + // Create a new session for this file + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + let arena = ast::Arena::new(); - // Enter the context and parse the file. - let _ = sess.enter(|| -> solar_interface::Result<()> { - // Set up the parser. - let arena = ast::Arena::new(); + // Enter the session context for this thread + let _ = sess.enter(|| -> solar_interface::Result<()> { + let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - for file in &self.input { - let lints = self.lints.clone(); + let ast = + parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - let mut parser = solar_parse::Parser::from_file(&sess, &arena, file) - .expect("Failed to create parser"); - let ast = parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + // Run all lints on the parsed AST and collect findings + for mut lint in lints { + let results = lint.lint(&ast); + local_findings.entry(lint).or_insert_with(Vec::new).extend(results); + } - // Run all lints on the parsed AST - for mut lint in lints { - let results = lint.lint(&ast); - findings.entry(lint.clone()).or_insert_with(Vec::new).extend(results); - } - } + Ok(()) + }); - // TODO: make the output nicer - for finding in findings { - let (lint, results) = finding; - let description = if self.description { lint.description() } else { "" }; + local_findings + }) + .collect::>>>(); - println!("{}: {}", lint.name(), description); - for result in results { - println!(" - {:?}", result); - } + let mut aggregated_findings = HashMap::new(); + for file_findings in all_findings { + for (lint, results) in file_findings { + aggregated_findings.entry(lint).or_insert_with(Vec::new).extend(results); } + } + + // TODO: make the output nicer + for finding in aggregated_findings { + let (lint, results) = finding; + let description = if self.description { lint.description() } else { "" }; - Ok(()) - }); + println!("{}: {}", lint.name(), description); + for result in results { + println!(" - {:?}", result); + } + } } } From 5b56b9103b16c455bb6c3cf5984a266ac24000b1 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 24 Dec 2024 02:06:42 -0500 Subject: [PATCH 023/107] reorder lint declarations --- crates/lint/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index c356c12e1807a..35dc2de727a1b 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -206,8 +206,6 @@ macro_rules! declare_lints { } declare_lints!( - // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), //High // Med (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), @@ -216,5 +214,7 @@ declare_lints!( (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), - (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description") + (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), + // Gas Optimizations + (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), ); From 57436d7d415d2531b2af08f0eed2be7427aee4c2 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 24 Dec 2024 02:09:12 -0500 Subject: [PATCH 024/107] clippy --- crates/lint/src/info.rs | 2 +- crates/lint/src/lib.rs | 9 ++++----- crates/lint/src/med.rs | 26 ++++++++++++-------------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/crates/lint/src/info.rs b/crates/lint/src/info.rs index 310fa38535655..ba0c415104c68 100644 --- a/crates/lint/src/info.rs +++ b/crates/lint/src/info.rs @@ -47,7 +47,7 @@ impl<'ast> Visit<'ast> for StructPascalCase { } } -impl<'ast> Visit<'ast> for FunctionCamelCase { +impl Visit<'_> for FunctionCamelCase { //TODO: visit item } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 35dc2de727a1b..9869f49d95abc 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -92,11 +92,10 @@ impl Linter { // TODO: make the output nicer for finding in aggregated_findings { let (lint, results) = finding; - let description = if self.description { lint.description() } else { "" }; + let _description = if self.description { lint.description() } else { "" }; - println!("{}: {}", lint.name(), description); - for result in results { - println!(" - {:?}", result); + for _result in results { + // TODO: display the finding } } } @@ -146,7 +145,7 @@ macro_rules! declare_lints { /// Lint a source unit and return the findings - pub fn lint<'ast>(&mut self, source_unit: &SourceUnit<'ast>) -> Vec { + pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { match self { $( Lint::$name(lint) => { diff --git a/crates/lint/src/med.rs b/crates/lint/src/med.rs index f5692bbaa53ac..e9c3808307ee0 100644 --- a/crates/lint/src/med.rs +++ b/crates/lint/src/med.rs @@ -8,7 +8,7 @@ use crate::DivideBeforeMultiply; impl<'ast> Visit<'ast> for DivideBeforeMultiply { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { - if self.contains_division(left_expr) { + if contains_division(left_expr) { self.items.push(expr.span); } } @@ -17,19 +17,17 @@ impl<'ast> Visit<'ast> for DivideBeforeMultiply { } } -impl DivideBeforeMultiply { - fn contains_division<'ast>(&self, expr: &'ast Expr<'ast>) -> bool { - match &expr.kind { - ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, - ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { - if let Some(inner_expr) = opt_expr { - self.contains_division(inner_expr) - } else { - false - } - }), - _ => false, - } +fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { + match &expr.kind { + ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, + ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { + if let Some(inner_expr) = opt_expr { + contains_division(inner_expr) + } else { + false + } + }), + _ => false, } } From e0e32b5a5262318f5c392ae0343aaa90a590e0a0 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 25 Dec 2024 00:35:48 -0500 Subject: [PATCH 025/107] add placeholder for additional lints --- crates/lint/src/gas.rs | 32 +++++++++++++++++++++++++++++++- crates/lint/src/high.rs | 14 ++++++++++++++ crates/lint/src/info.rs | 4 +++- crates/lint/src/lib.rs | 7 +++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 crates/lint/src/high.rs diff --git a/crates/lint/src/gas.rs b/crates/lint/src/gas.rs index ac9b1462a0cc7..1d0c6ab2da816 100644 --- a/crates/lint/src/gas.rs +++ b/crates/lint/src/gas.rs @@ -3,7 +3,9 @@ use solar_ast::{ visit::Visit, }; -use crate::AsmKeccak256; +use crate::{ + AsmKeccak256, PackStorageVariables, PackStructs, UseConstantVariable, UseImmutableVariable, +}; impl<'ast> Visit<'ast> for AsmKeccak256 { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { @@ -18,6 +20,34 @@ impl<'ast> Visit<'ast> for AsmKeccak256 { } } +impl<'ast> Visit<'ast> for PackStorageVariables { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for PackStructs { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for UseConstantVariable { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for UseImmutableVariable { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +// TODO: public function could be declared external + +// TODO: avoid using `this` to read public variables + #[cfg(test)] mod test { use solar_ast::{ast, visit::Visit}; diff --git a/crates/lint/src/high.rs b/crates/lint/src/high.rs new file mode 100644 index 0000000000000..bc5bb186d5436 --- /dev/null +++ b/crates/lint/src/high.rs @@ -0,0 +1,14 @@ +use crate::{ArbitraryTransferFrom, IncorrectShift}; +use solar_ast::{ast::Expr, visit::Visit}; + +impl<'ast> Visit<'ast> for IncorrectShift { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for ArbitraryTransferFrom { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} diff --git a/crates/lint/src/info.rs b/crates/lint/src/info.rs index ba0c415104c68..7fc0f6239adcc 100644 --- a/crates/lint/src/info.rs +++ b/crates/lint/src/info.rs @@ -48,7 +48,9 @@ impl<'ast> Visit<'ast> for StructPascalCase { } impl Visit<'_> for FunctionCamelCase { - //TODO: visit item + fn visit_function_header(&mut self, header: &'_ solar_ast::ast::FunctionHeader<'_>) { + todo!() + } } // Check if a string is camelCase diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 9869f49d95abc..709e014b00b01 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,4 +1,5 @@ pub mod gas; +pub mod high; pub mod info; pub mod med; @@ -206,6 +207,8 @@ macro_rules! declare_lints { declare_lints!( //High + (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), + (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), // Med (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), // Low @@ -216,4 +219,8 @@ declare_lints!( (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), // Gas Optimizations (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), + (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), + (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), + (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), + (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), ); From e522ecb702fa8bc65cb7ec26712913a3d2716ef3 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 25 Dec 2024 00:38:31 -0500 Subject: [PATCH 026/107] more placeholders --- crates/lint/src/gas.rs | 15 +++++++++++++-- crates/lint/src/lib.rs | 7 +++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/lint/src/gas.rs b/crates/lint/src/gas.rs index 1d0c6ab2da816..cb6540e151da1 100644 --- a/crates/lint/src/gas.rs +++ b/crates/lint/src/gas.rs @@ -4,7 +4,8 @@ use solar_ast::{ }; use crate::{ - AsmKeccak256, PackStorageVariables, PackStructs, UseConstantVariable, UseImmutableVariable, + AsmKeccak256, AvoidUsingThis, PackStorageVariables, PackStructs, UseConstantVariable, + UseExternalVisibility, UseImmutableVariable, }; impl<'ast> Visit<'ast> for AsmKeccak256 { @@ -44,7 +45,17 @@ impl<'ast> Visit<'ast> for UseImmutableVariable { } } -// TODO: public function could be declared external +impl<'ast> Visit<'ast> for UseExternalVisibility { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for AvoidUsingThis { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} // TODO: avoid using `this` to read public variables diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 709e014b00b01..3f1979b865fc1 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -223,4 +223,11 @@ declare_lints!( (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), + (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description"), + ( + AvoidUsingThis, + Severity::Gas, + "avoid-using-this", + "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL." + ), ); From 200ba8667e25c93115dafe879f8f2c9a67ae092f Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 00:16:12 -0500 Subject: [PATCH 027/107] wip --- crates/forge/bin/cmd/lint.rs | 10 ++++++++ crates/lint/src/lib.rs | 46 +++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 7ca9d97113f13..ed4fd0e32d872 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -87,6 +87,7 @@ impl LintArgs { bail!("No source files found in path"); } + // TODO: maybe compile and lint on the aggreagted compiler output? Linter::new(input) .with_severity(self.severity) .with_description(self.with_description) @@ -95,3 +96,12 @@ impl LintArgs { Ok(()) } } + +pub struct ProjectLinter {} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SourceLocation { + pub file: String, + pub start: i32, + pub end: i32, +} diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 3f1979b865fc1..a1a73ec346f33 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -4,7 +4,12 @@ pub mod info; pub mod med; use rayon::prelude::*; -use std::{collections::HashMap, hash::Hasher, path::PathBuf}; +use std::{ + collections::{BTreeMap, HashMap}, + error::Error, + hash::Hasher, + path::PathBuf, +}; use clap::ValueEnum; use solar_ast::{ @@ -28,6 +33,45 @@ pub enum Severity { Gas, } +pub struct ProjectLinter {} + +/// The main compiler abstraction trait. +/// +/// Currently mostly represents a wrapper around compiler binary aware of the version and able to +/// compile given input into [`CompilerOutput`] including artifacts and errors. +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait Linter: Send + Sync + Clone { + // TODO: keep this + /// Input type for the compiler. Contains settings and sources to be compiled. + type Input: CompilerInput; + /// Compiler settings. + type Settings: LinterSettings; + + type LinterError: Error; + /// Enum of languages supported by the linter. + type Language: Language; + /// Main entrypoint for the linter. + fn lint(&self, input: &Self::Input) -> Result; +} + +pub struct LinterOutput { + pub results: BTreeMap>, +} + +/// Linter for languages supported by the Solc compiler +pub struct SolcLinter {} + +impl Language for SolcLinter { + const FILE_EXTENSIONS: &'static [&'static str] = SOLC_EXTENSIONS; +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SourceLocation { + pub file: String, + pub start: i32, + pub end: i32, +} + pub struct Linter { pub input: Vec, pub lints: Vec, From 869d6229375b7868c6ffcc81553dda30d845aef8 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 00:32:11 -0500 Subject: [PATCH 028/107] wip --- crates/forge/bin/cmd/lint.rs | 13 +- crates/lint/src/lib.rs | 465 +++++++++++++++++++---------------- 2 files changed, 255 insertions(+), 223 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index ed4fd0e32d872..7496a9c4a7057 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -52,6 +52,10 @@ impl_figment_convert_basic!(LintArgs); impl LintArgs { pub fn run(self) -> Result<()> { let config = self.try_load_config_emit_warnings()?; + + // Set up the project. + let project = config.project()?; + let root = if let Some(root) = &self.root { root } else { &config.root }; // Expand ignore globs and canonicalize paths @@ -96,12 +100,3 @@ impl LintArgs { Ok(()) } } - -pub struct ProjectLinter {} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct SourceLocation { - pub file: String, - pub start: i32, - pub end: i32, -} diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index a1a73ec346f33..7995888c06bf1 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -7,7 +7,8 @@ use rayon::prelude::*; use std::{ collections::{BTreeMap, HashMap}, error::Error, - hash::Hasher, + hash::{Hash, Hasher}, + marker::PhantomData, path::PathBuf, }; @@ -33,12 +34,48 @@ pub enum Severity { Gas, } -pub struct ProjectLinter {} +pub struct ProjectLinter +where + L: Linter, +{ + // TODO: remove later + phantom: PhantomData, +} + +impl ProjectLinter +where + L: Linter, +{ + // pub fn new() -> Self { + // Self {} + // } + + /// Lints the project. + pub fn lint>( + mut self, + project: &Project, + ) -> Result { + if !project.paths.has_input_files() && self.files.is_empty() { + sh_println!("Nothing to compile")?; + // nothing to do here + std::process::exit(0); + } -/// The main compiler abstraction trait. -/// -/// Currently mostly represents a wrapper around compiler binary aware of the version and able to -/// compile given input into [`CompilerOutput`] including artifacts and errors. + // Taking is fine since we don't need these in `compile_with`. + let files = std::mem::take(&mut self.files); + self.compile_with(|| { + let sources = if !files.is_empty() { + Source::read_all(files)? + } else { + project.paths.read_input_files()? + }; + + todo!() + }) + } +} + +/// The main linter abstraction trait #[auto_impl::auto_impl(&, Box, Arc)] pub trait Linter: Send + Sync + Clone { // TODO: keep this @@ -46,18 +83,21 @@ pub trait Linter: Send + Sync + Clone { type Input: CompilerInput; /// Compiler settings. type Settings: LinterSettings; - type LinterError: Error; /// Enum of languages supported by the linter. type Language: Language; + /// Main entrypoint for the linter. fn lint(&self, input: &Self::Input) -> Result; } -pub struct LinterOutput { - pub results: BTreeMap>, +// TODO: make Lint a generic +pub struct LinterOutput { + pub results: BTreeMap>, } +pub trait Lint: Hash {} + /// Linter for languages supported by the Solc compiler pub struct SolcLinter {} @@ -68,210 +108,207 @@ impl Language for SolcLinter { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SourceLocation { pub file: String, - pub start: i32, - pub end: i32, -} - -pub struct Linter { - pub input: Vec, - pub lints: Vec, - pub description: bool, -} - -impl Linter { - pub fn new(input: Vec) -> Self { - Self { input, lints: Lint::all(), description: false } - } - - pub fn with_severity(mut self, severity: Option>) -> Self { - if let Some(severity) = severity { - self.lints.retain(|lint| severity.contains(&lint.severity())); - } - self - } - - pub fn with_description(mut self, description: bool) -> Self { - self.description = description; - self - } - - pub fn lint(self) { - let all_findings = self - .input - .par_iter() - .map(|file| { - let lints = self.lints.clone(); - let mut local_findings = HashMap::new(); - - // Create a new session for this file - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - let arena = ast::Arena::new(); - - // Enter the session context for this thread - let _ = sess.enter(|| -> solar_interface::Result<()> { - let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - - let ast = - parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - - // Run all lints on the parsed AST and collect findings - for mut lint in lints { - let results = lint.lint(&ast); - local_findings.entry(lint).or_insert_with(Vec::new).extend(results); - } - - Ok(()) - }); - - local_findings - }) - .collect::>>>(); - - let mut aggregated_findings = HashMap::new(); - for file_findings in all_findings { - for (lint, results) in file_findings { - aggregated_findings.entry(lint).or_insert_with(Vec::new).extend(results); - } - } - - // TODO: make the output nicer - for finding in aggregated_findings { - let (lint, results) = finding; - let _description = if self.description { lint.description() } else { "" }; - - for _result in results { - // TODO: display the finding - } - } - } -} - -macro_rules! declare_lints { - ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum Lint { - $( - $name($name), - )* - } - - impl Lint { - pub fn all() -> Vec { - vec![ - $( - Lint::$name($name::new()), - )* - ] - } - - pub fn severity(&self) -> Severity { - match self { - $( - Lint::$name(_) => $severity, - )* - } - } - - pub fn name(&self) -> &'static str { - match self { - $( - Lint::$name(_) => $lint_name, - )* - } - } - - pub fn description(&self) -> &'static str { - match self { - $( - Lint::$name(_) => $description, - )* - } - } - - - /// Lint a source unit and return the findings - pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { - match self { - $( - Lint::$name(lint) => { - lint.visit_source_unit(source_unit); - lint.items.clone() - }, - )* - } - } - } - - impl<'ast> Visit<'ast> for Lint { - fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { - match self { - $( - Lint::$name(lint) => lint.visit_source_unit(source_unit), - )* - } - } - } - - - impl std::hash::Hash for Lint { - fn hash(&self, state: &mut H) { - self.name().hash(state); - } - } - - $( - #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] - pub struct $name { - pub items: Vec, - } - - impl $name { - pub fn new() -> Self { - Self { items: Vec::new() } - } - - /// Returns the severity of the lint - pub fn severity() -> Severity { - $severity - } - - /// Returns the name of the lint - pub fn name() -> &'static str { - $lint_name - } - - /// Returns the description of the lint - pub fn description() -> &'static str { - $description - } - } - )* - }; + pub span: Span, } -declare_lints!( - //High - (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), - (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), - // Med - (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), - // Low - // Info - (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), - (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), - (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), - (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), - // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), - (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), - (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), - (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), - (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), - (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description"), - ( - AvoidUsingThis, - Severity::Gas, - "avoid-using-this", - "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL." - ), -); +// pub struct Linter { +// pub input: Vec, +// pub lints: Vec, +// pub description: bool, +// } + +// impl Linter { +// pub fn new(input: Vec) -> Self { +// Self { input, lints: Lint::all(), description: false } +// } + +// pub fn with_severity(mut self, severity: Option>) -> Self { +// if let Some(severity) = severity { +// self.lints.retain(|lint| severity.contains(&lint.severity())); +// } +// self +// } + +// pub fn with_description(mut self, description: bool) -> Self { +// self.description = description; +// self +// } + +// pub fn lint(self) { +// let all_findings = self +// .input +// .par_iter() +// .map(|file| { +// let lints = self.lints.clone(); +// let mut local_findings = HashMap::new(); + +// // Create a new session for this file +// let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); +// let arena = ast::Arena::new(); + +// // Enter the session context for this thread +// let _ = sess.enter(|| -> solar_interface::Result<()> { +// let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + +// let ast = +// parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + +// // Run all lints on the parsed AST and collect findings +// for mut lint in lints { +// let results = lint.lint(&ast); +// local_findings.entry(lint).or_insert_with(Vec::new).extend(results); +// } + +// Ok(()) +// }); + +// local_findings +// }) +// .collect::>>>(); + +// let mut aggregated_findings = HashMap::new(); +// for file_findings in all_findings { +// for (lint, results) in file_findings { +// aggregated_findings.entry(lint).or_insert_with(Vec::new).extend(results); +// } +// } + +// // TODO: make the output nicer +// for finding in aggregated_findings { +// let (lint, results) = finding; +// let _description = if self.description { lint.description() } else { "" }; + +// for _result in results { +// // TODO: display the finding +// } +// } +// } +// } + +// macro_rules! declare_lints { +// ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { +// #[derive(Debug, Clone, PartialEq, Eq)] +// pub enum Lint { +// $( +// $name($name), +// )* +// } + +// impl Lint { +// pub fn all() -> Vec { +// vec![ +// $( +// Lint::$name($name::new()), +// )* +// ] +// } + +// pub fn severity(&self) -> Severity { +// match self { +// $( +// Lint::$name(_) => $severity, +// )* +// } +// } + +// pub fn name(&self) -> &'static str { +// match self { +// $( +// Lint::$name(_) => $lint_name, +// )* +// } +// } + +// pub fn description(&self) -> &'static str { +// match self { +// $( +// Lint::$name(_) => $description, +// )* +// } +// } + +// /// Lint a source unit and return the findings +// pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { +// match self { +// $( +// Lint::$name(lint) => { +// lint.visit_source_unit(source_unit); +// lint.items.clone() +// }, +// )* +// } +// } +// } + +// impl<'ast> Visit<'ast> for Lint { +// fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { +// match self { +// $( +// Lint::$name(lint) => lint.visit_source_unit(source_unit), +// )* +// } +// } +// } + +// impl std::hash::Hash for Lint { +// fn hash(&self, state: &mut H) { +// self.name().hash(state); +// } +// } + +// $( +// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +// pub struct $name { +// pub items: Vec, +// } + +// impl $name { +// pub fn new() -> Self { +// Self { items: Vec::new() } +// } + +// /// Returns the severity of the lint +// pub fn severity() -> Severity { +// $severity +// } + +// /// Returns the name of the lint +// pub fn name() -> &'static str { +// $lint_name +// } + +// /// Returns the description of the lint +// pub fn description() -> &'static str { +// $description +// } +// } +// )* +// }; +// } + +// declare_lints!( +// //High +// (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), +// (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), +// // Med +// (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), +// // Low +// // Info +// (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), +// (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), +// (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), +// (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), +// // Gas Optimizations +// (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), +// (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), +// (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), +// (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), +// (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), +// (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description"), +// ( +// AvoidUsingThis, +// Severity::Gas, +// "avoid-using-this", +// "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL." +// ), +// ); From eb249557f57bf66e68db51626ebc30b23b4b13b6 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 01:27:08 -0500 Subject: [PATCH 029/107] refactor into sol linter --- Cargo.lock | 5 ++ crates/lint/Cargo.toml | 9 +++- crates/lint/src/gas.rs | 93 ------------------------------------- crates/lint/src/high.rs | 14 ------ crates/lint/src/info.rs | 72 ---------------------------- crates/lint/src/lib.rs | 92 ++++++++++++++++++++---------------- crates/lint/src/med.rs | 68 --------------------------- crates/lint/src/sol/gas.rs | 93 +++++++++++++++++++++++++++++++++++++ crates/lint/src/sol/high.rs | 14 ++++++ crates/lint/src/sol/info.rs | 72 ++++++++++++++++++++++++++++ crates/lint/src/sol/med.rs | 68 +++++++++++++++++++++++++++ crates/lint/src/sol/mod.rs | 6 +++ 12 files changed, 317 insertions(+), 289 deletions(-) delete mode 100644 crates/lint/src/gas.rs delete mode 100644 crates/lint/src/high.rs delete mode 100644 crates/lint/src/info.rs delete mode 100644 crates/lint/src/med.rs create mode 100644 crates/lint/src/sol/gas.rs create mode 100644 crates/lint/src/sol/high.rs create mode 100644 crates/lint/src/sol/info.rs create mode 100644 crates/lint/src/sol/med.rs create mode 100644 crates/lint/src/sol/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1a0dc19a078e8..2899bc88159d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3558,10 +3558,15 @@ dependencies = [ name = "forge-lint" version = "0.3.0" dependencies = [ + "auto_impl", "clap", "eyre", + "foundry-common", + "foundry-compilers", "rayon", "regex", + "serde", + "serde_json", "solar-ast", "solar-interface", "solar-parse", diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index a5a6ab72f8352..92a8de1631c4d 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -14,11 +14,18 @@ repository.workspace = true workspace = true [dependencies] +# lib +foundry-common.workspace = true +foundry-compilers.workspace = true + solar-parse.workspace = true solar-ast.workspace = true solar-interface.workspace = true + eyre.workspace = true rayon.workspace = true - +serde_json.workspace = true +auto_impl.workspace = true +serde = { workspace = true, features = ["derive"] } regex = "1.11" clap = { version = "4", features = ["derive"] } diff --git a/crates/lint/src/gas.rs b/crates/lint/src/gas.rs deleted file mode 100644 index cb6540e151da1..0000000000000 --- a/crates/lint/src/gas.rs +++ /dev/null @@ -1,93 +0,0 @@ -use solar_ast::{ - ast::{Expr, ExprKind}, - visit::Visit, -}; - -use crate::{ - AsmKeccak256, AvoidUsingThis, PackStorageVariables, PackStructs, UseConstantVariable, - UseExternalVisibility, UseImmutableVariable, -}; - -impl<'ast> Visit<'ast> for AsmKeccak256 { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - if let ExprKind::Call(expr, _) = &expr.kind { - if let ExprKind::Ident(ident) = &expr.kind { - if ident.name.as_str() == "keccak256" { - self.items.push(expr.span); - } - } - } - self.walk_expr(expr); - } -} - -impl<'ast> Visit<'ast> for PackStorageVariables { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -impl<'ast> Visit<'ast> for PackStructs { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -impl<'ast> Visit<'ast> for UseConstantVariable { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -impl<'ast> Visit<'ast> for UseImmutableVariable { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -impl<'ast> Visit<'ast> for UseExternalVisibility { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -impl<'ast> Visit<'ast> for AvoidUsingThis { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -// TODO: avoid using `this` to read public variables - -#[cfg(test)] -mod test { - use solar_ast::{ast, visit::Visit}; - use solar_interface::{ColorChoice, Session}; - use std::path::Path; - - use crate::AsmKeccak256; - - #[test] - fn test_keccak256() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = ast::Arena::new(); - - let mut parser = - solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol"))?; - - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit())?; - - let mut pattern = AsmKeccak256::default(); - pattern.visit_source_unit(&ast); - - assert_eq!(pattern.items.len(), 2); - - Ok(()) - }); - - Ok(()) - } -} diff --git a/crates/lint/src/high.rs b/crates/lint/src/high.rs deleted file mode 100644 index bc5bb186d5436..0000000000000 --- a/crates/lint/src/high.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::{ArbitraryTransferFrom, IncorrectShift}; -use solar_ast::{ast::Expr, visit::Visit}; - -impl<'ast> Visit<'ast> for IncorrectShift { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} - -impl<'ast> Visit<'ast> for ArbitraryTransferFrom { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() - } -} diff --git a/crates/lint/src/info.rs b/crates/lint/src/info.rs deleted file mode 100644 index 7fc0f6239adcc..0000000000000 --- a/crates/lint/src/info.rs +++ /dev/null @@ -1,72 +0,0 @@ -use regex::Regex; - -use solar_ast::{ - ast::{ItemStruct, VariableDefinition}, - visit::Visit, -}; - -use crate::{FunctionCamelCase, StructPascalCase, VariableCamelCase, VariableCapsCase}; - -impl<'ast> Visit<'ast> for VariableCamelCase { - fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { - if let Some(mutability) = var.mutability { - if !mutability.is_constant() && !mutability.is_immutable() { - if let Some(name) = var.name { - if !is_camel_case(name.as_str()) { - self.items.push(var.span); - } - } - } - } - self.walk_variable_definition(var); - } -} - -impl<'ast> Visit<'ast> for VariableCapsCase { - fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { - if let Some(mutability) = var.mutability { - if mutability.is_constant() || mutability.is_immutable() { - if let Some(name) = var.name { - if !is_caps_case(name.as_str()) { - self.items.push(var.span); - } - } - } - } - self.walk_variable_definition(var); - } -} - -impl<'ast> Visit<'ast> for StructPascalCase { - fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { - if !is_pascal_case(strukt.name.as_str()) { - self.items.push(strukt.name.span); - } - - self.walk_item_struct(strukt); - } -} - -impl Visit<'_> for FunctionCamelCase { - fn visit_function_header(&mut self, header: &'_ solar_ast::ast::FunctionHeader<'_>) { - todo!() - } -} - -// Check if a string is camelCase -pub fn is_camel_case(s: &str) -> bool { - let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); - re.is_match(s) && s.chars().any(|c| c.is_uppercase()) -} - -// Check if a string is PascalCase -pub fn is_pascal_case(s: &str) -> bool { - let re = Regex::new(r"^[A-Z0-9][a-zA-Z0-9]*$").unwrap(); - re.is_match(s) -} - -// Check if a string is SCREAMING_SNAKE_CASE -pub fn is_caps_case(s: &str) -> bool { - let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); - re.is_match(s) && s.contains('_') -} diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 7995888c06bf1..5f3827532eb87 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,9 +1,11 @@ -pub mod gas; -pub mod high; -pub mod info; -pub mod med; +pub mod sol; +use foundry_common::sh_println; +use foundry_compilers::{ + artifacts::Contract, Compiler, CompilerContract, CompilerInput, Language, Project, +}; use rayon::prelude::*; +use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, error::Error, @@ -38,6 +40,10 @@ pub struct ProjectLinter where L: Linter, { + /// Extra files to include, that are not necessarily in the project's source dir. + files: Vec, + severity: Option>, + description: bool, // TODO: remove later phantom: PhantomData, } @@ -46,63 +52,73 @@ impl ProjectLinter where L: Linter, { - // pub fn new() -> Self { - // Self {} - // } - /// Lints the project. pub fn lint>( mut self, project: &Project, - ) -> Result { + ) -> eyre::Result> { if !project.paths.has_input_files() && self.files.is_empty() { sh_println!("Nothing to compile")?; // nothing to do here std::process::exit(0); } - // Taking is fine since we don't need these in `compile_with`. - let files = std::mem::take(&mut self.files); - self.compile_with(|| { - let sources = if !files.is_empty() { - Source::read_all(files)? - } else { - project.paths.read_input_files()? - }; - - todo!() - }) + // // Taking is fine since we don't need these in `compile_with`. + // let files = std::mem::take(&mut self.files); + // self.compile_with(|| { + // let sources = if !files.is_empty() { + // Source::read_all(files)? + // } else { + // project.paths.read_input_files()? + // }; + + // }) + + todo!() + } + + pub fn with_description(mut self, description: bool) -> Self { + self.description = description; + self + } + + pub fn with_severity(mut self, severity: Option>) -> Self { + self.severity = severity; + self } } +// NOTE: add some way to specify linter profiles. For example having a profile adhering to the op stack, base, etc. +// This can probably also be accomplished via the foundry.toml. Maybe have generic profile/settings + /// The main linter abstraction trait -#[auto_impl::auto_impl(&, Box, Arc)] pub trait Linter: Send + Sync + Clone { - // TODO: keep this /// Input type for the compiler. Contains settings and sources to be compiled. - type Input: CompilerInput; - /// Compiler settings. - type Settings: LinterSettings; + type Input: CompilerInput; + + // TODO: probably remove + // /// TODO: Add docs. This represents linter settings. (ex. Default, OP Stack, etc.) + // type Settings: LinterSettings; + type Lint: Lint; type LinterError: Error; /// Enum of languages supported by the linter. type Language: Language; /// Main entrypoint for the linter. - fn lint(&self, input: &Self::Input) -> Result; + fn lint(&self, input: &Self::Input) -> Result, Self::LinterError>; } -// TODO: make Lint a generic -pub struct LinterOutput { - pub results: BTreeMap>, +// TODO: probably remove +pub trait LinterSettings { + fn lints() -> Vec; } -pub trait Lint: Hash {} - -/// Linter for languages supported by the Solc compiler -pub struct SolcLinter {} +pub struct LinterOutput { + pub results: BTreeMap>, +} -impl Language for SolcLinter { - const FILE_EXTENSIONS: &'static [&'static str] = SOLC_EXTENSIONS; +pub trait Lint: Hash { + fn results(&self) -> Vec; } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -111,12 +127,6 @@ pub struct SourceLocation { pub span: Span, } -// pub struct Linter { -// pub input: Vec, -// pub lints: Vec, -// pub description: bool, -// } - // impl Linter { // pub fn new(input: Vec) -> Self { // Self { input, lints: Lint::all(), description: false } diff --git a/crates/lint/src/med.rs b/crates/lint/src/med.rs deleted file mode 100644 index e9c3808307ee0..0000000000000 --- a/crates/lint/src/med.rs +++ /dev/null @@ -1,68 +0,0 @@ -use solar_ast::{ - ast::{BinOp, BinOpKind, Expr, ExprKind}, - visit::Visit, -}; - -use crate::DivideBeforeMultiply; - -impl<'ast> Visit<'ast> for DivideBeforeMultiply { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { - if contains_division(left_expr) { - self.items.push(expr.span); - } - } - - self.walk_expr(expr); - } -} - -fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { - match &expr.kind { - ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, - ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { - if let Some(inner_expr) = opt_expr { - contains_division(inner_expr) - } else { - false - } - }), - _ => false, - } -} - -#[cfg(test)] -mod test { - use solar_ast::{ast, visit::Visit}; - use solar_interface::{ColorChoice, Session}; - use std::path::Path; - - use crate::DivideBeforeMultiply; - - #[test] - fn test_divide_before_multiply() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = ast::Arena::new(); - - let mut parser = solar_parse::Parser::from_file( - &sess, - &arena, - Path::new("testdata/DivideBeforeMultiply.sol"), - )?; - - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit())?; - - let mut pattern = DivideBeforeMultiply::default(); - pattern.visit_source_unit(&ast); - - assert_eq!(pattern.items.len(), 6); - - Ok(()) - }); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs new file mode 100644 index 0000000000000..66d4651645a50 --- /dev/null +++ b/crates/lint/src/sol/gas.rs @@ -0,0 +1,93 @@ +// use solar_ast::{ +// ast::{Expr, ExprKind}, +// visit::Visit, +// }; + +// use crate::{ +// AsmKeccak256, AvoidUsingThis, PackStorageVariables, PackStructs, UseConstantVariable, +// UseExternalVisibility, UseImmutableVariable, +// }; + +// impl<'ast> Visit<'ast> for AsmKeccak256 { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// if let ExprKind::Call(expr, _) = &expr.kind { +// if let ExprKind::Ident(ident) = &expr.kind { +// if ident.name.as_str() == "keccak256" { +// self.items.push(expr.span); +// } +// } +// } +// self.walk_expr(expr); +// } +// } + +// impl<'ast> Visit<'ast> for PackStorageVariables { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// impl<'ast> Visit<'ast> for PackStructs { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// impl<'ast> Visit<'ast> for UseConstantVariable { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// impl<'ast> Visit<'ast> for UseImmutableVariable { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// impl<'ast> Visit<'ast> for UseExternalVisibility { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// impl<'ast> Visit<'ast> for AvoidUsingThis { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// // TODO: avoid using `this` to read public variables + +// #[cfg(test)] +// mod test { +// use solar_ast::{ast, visit::Visit}; +// use solar_interface::{ColorChoice, Session}; +// use std::path::Path; + +// use crate::AsmKeccak256; + +// #[test] +// fn test_keccak256() -> eyre::Result<()> { +// let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + +// let _ = sess.enter(|| -> solar_interface::Result<()> { +// let arena = ast::Arena::new(); + +// let mut parser = +// solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol"))?; + +// // Parse the file. +// let ast = parser.parse_file().map_err(|e| e.emit())?; + +// let mut pattern = AsmKeccak256::default(); +// pattern.visit_source_unit(&ast); + +// assert_eq!(pattern.items.len(), 2); + +// Ok(()) +// }); + +// Ok(()) +// } +// } diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high.rs new file mode 100644 index 0000000000000..6a43a33aff04c --- /dev/null +++ b/crates/lint/src/sol/high.rs @@ -0,0 +1,14 @@ +// use crate::{ArbitraryTransferFrom, IncorrectShift}; +// use solar_ast::{ast::Expr, visit::Visit}; + +// impl<'ast> Visit<'ast> for IncorrectShift { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } + +// impl<'ast> Visit<'ast> for ArbitraryTransferFrom { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// todo!() +// } +// } diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs new file mode 100644 index 0000000000000..740951d9adb13 --- /dev/null +++ b/crates/lint/src/sol/info.rs @@ -0,0 +1,72 @@ +// use regex::Regex; + +// use solar_ast::{ +// ast::{ItemStruct, VariableDefinition}, +// visit::Visit, +// }; + +// use crate::{FunctionCamelCase, StructPascalCase, VariableCamelCase, VariableCapsCase}; + +// impl<'ast> Visit<'ast> for VariableCamelCase { +// fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { +// if let Some(mutability) = var.mutability { +// if !mutability.is_constant() && !mutability.is_immutable() { +// if let Some(name) = var.name { +// if !is_camel_case(name.as_str()) { +// self.items.push(var.span); +// } +// } +// } +// } +// self.walk_variable_definition(var); +// } +// } + +// impl<'ast> Visit<'ast> for VariableCapsCase { +// fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { +// if let Some(mutability) = var.mutability { +// if mutability.is_constant() || mutability.is_immutable() { +// if let Some(name) = var.name { +// if !is_caps_case(name.as_str()) { +// self.items.push(var.span); +// } +// } +// } +// } +// self.walk_variable_definition(var); +// } +// } + +// impl<'ast> Visit<'ast> for StructPascalCase { +// fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { +// if !is_pascal_case(strukt.name.as_str()) { +// self.items.push(strukt.name.span); +// } + +// self.walk_item_struct(strukt); +// } +// } + +// impl Visit<'_> for FunctionCamelCase { +// fn visit_function_header(&mut self, header: &'_ solar_ast::ast::FunctionHeader<'_>) { +// todo!() +// } +// } + +// // Check if a string is camelCase +// pub fn is_camel_case(s: &str) -> bool { +// let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); +// re.is_match(s) && s.chars().any(|c| c.is_uppercase()) +// } + +// // Check if a string is PascalCase +// pub fn is_pascal_case(s: &str) -> bool { +// let re = Regex::new(r"^[A-Z0-9][a-zA-Z0-9]*$").unwrap(); +// re.is_match(s) +// } + +// // Check if a string is SCREAMING_SNAKE_CASE +// pub fn is_caps_case(s: &str) -> bool { +// let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); +// re.is_match(s) && s.contains('_') +// } diff --git a/crates/lint/src/sol/med.rs b/crates/lint/src/sol/med.rs new file mode 100644 index 0000000000000..bfd5902a82330 --- /dev/null +++ b/crates/lint/src/sol/med.rs @@ -0,0 +1,68 @@ +// use solar_ast::{ +// ast::{BinOp, BinOpKind, Expr, ExprKind}, +// visit::Visit, +// }; + +// use crate::DivideBeforeMultiply; + +// impl<'ast> Visit<'ast> for DivideBeforeMultiply { +// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { +// if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { +// if contains_division(left_expr) { +// self.items.push(expr.span); +// } +// } + +// self.walk_expr(expr); +// } +// } + +// fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { +// match &expr.kind { +// ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, +// ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { +// if let Some(inner_expr) = opt_expr { +// contains_division(inner_expr) +// } else { +// false +// } +// }), +// _ => false, +// } +// } + +// #[cfg(test)] +// mod test { +// use solar_ast::{ast, visit::Visit}; +// use solar_interface::{ColorChoice, Session}; +// use std::path::Path; + +// use crate::DivideBeforeMultiply; + +// #[test] +// fn test_divide_before_multiply() -> eyre::Result<()> { +// let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + +// let _ = sess.enter(|| -> solar_interface::Result<()> { +// let arena = ast::Arena::new(); + +// let mut parser = solar_parse::Parser::from_file( +// &sess, +// &arena, +// Path::new("testdata/DivideBeforeMultiply.sol"), +// )?; + +// // Parse the file. +// let ast = parser.parse_file().map_err(|e| e.emit())?; + +// let mut pattern = DivideBeforeMultiply::default(); +// pattern.visit_source_unit(&ast); + +// assert_eq!(pattern.items.len(), 6); + +// Ok(()) +// }); + +// Ok(()) +// } +// } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs new file mode 100644 index 0000000000000..91efba7fe273c --- /dev/null +++ b/crates/lint/src/sol/mod.rs @@ -0,0 +1,6 @@ +pub mod gas; +pub mod high; +pub mod info; +pub mod med; + +pub struct SolidityLinter {} From 39e6b54540d2a0a13d50ff01e02d847251a2df2b Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 01:57:57 -0500 Subject: [PATCH 030/107] impl Linter for SolidityLinter --- Cargo.lock | 1 + crates/lint/Cargo.toml | 1 + crates/lint/src/lib.rs | 34 ++++++++++++-------- crates/lint/src/sol/mod.rs | 66 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2899bc88159d6..78d3460fda9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3570,6 +3570,7 @@ dependencies = [ "solar-ast", "solar-interface", "solar-parse", + "thiserror 2.0.9", ] [[package]] diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 92a8de1631c4d..2e9fcb22601d4 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -24,6 +24,7 @@ solar-interface.workspace = true eyre.workspace = true rayon.workspace = true +thiserror.workspace = true serde_json.workspace = true auto_impl.workspace = true serde = { workspace = true, features = ["derive"] } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 5f3827532eb87..2c8f7c1c764cf 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -2,7 +2,8 @@ pub mod sol; use foundry_common::sh_println; use foundry_compilers::{ - artifacts::Contract, Compiler, CompilerContract, CompilerInput, Language, Project, + artifacts::{Contract, Source}, + Compiler, CompilerContract, CompilerInput, Language, Project, }; use rayon::prelude::*; use serde::{Deserialize, Serialize}; @@ -40,18 +41,21 @@ pub struct ProjectLinter where L: Linter, { + pub linter: L, /// Extra files to include, that are not necessarily in the project's source dir. - files: Vec, - severity: Option>, - description: bool, - // TODO: remove later - phantom: PhantomData, + pub files: Vec, + pub severity: Option>, + pub description: bool, } impl ProjectLinter where L: Linter, { + pub fn new(linter: L) -> Self { + Self { linter, files: Vec::new(), severity: None, description: false } + } + /// Lints the project. pub fn lint>( mut self, @@ -74,6 +78,14 @@ where // }) + let sources = if !self.files.is_empty() { + Source::read_all(self.files.clone())? + } else { + project.paths.read_input_files()? + }; + + let input = sources.into_iter().map(|(path, _)| path).collect::>(); + todo!() } @@ -89,15 +101,11 @@ where } // NOTE: add some way to specify linter profiles. For example having a profile adhering to the op stack, base, etc. -// This can probably also be accomplished via the foundry.toml. Maybe have generic profile/settings +// This can probably also be accomplished via the foundry.toml or some functions. Maybe have generic profile/settings /// The main linter abstraction trait pub trait Linter: Send + Sync + Clone { - /// Input type for the compiler. Contains settings and sources to be compiled. - type Input: CompilerInput; - - // TODO: probably remove - // /// TODO: Add docs. This represents linter settings. (ex. Default, OP Stack, etc.) + // TODO: Add docs. This represents linter settings. (ex. Default, OP Stack, etc. // type Settings: LinterSettings; type Lint: Lint; type LinterError: Error; @@ -105,7 +113,7 @@ pub trait Linter: Send + Sync + Clone { type Language: Language; /// Main entrypoint for the linter. - fn lint(&self, input: &Self::Input) -> Result, Self::LinterError>; + fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; } // TODO: probably remove diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 91efba7fe273c..bb5604f0abd1c 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,6 +1,72 @@ +use std::path::PathBuf; + +use eyre::Error; +use foundry_compilers::solc::SolcLanguage; +use solar_interface::{ + diagnostics::{DiagnosticBuilder, ErrorGuaranteed}, + ColorChoice, Session, +}; +use thiserror::Error; + +use crate::{Lint, Linter, LinterOutput, SourceLocation}; + pub mod gas; pub mod high; pub mod info; pub mod med; +#[derive(Debug, Hash)] +pub enum SolLint {} + +impl Lint for SolLint { + fn results(&self) -> Vec { + todo!() + } +} + +#[derive(Debug, Clone)] pub struct SolidityLinter {} + +#[derive(Error, Debug)] +pub enum SolLintError {} + +impl Linter for SolidityLinter { + // TODO: update this to be a solar error + type LinterError = SolLintError; + type Lint = SolLint; + type Language = SolcLanguage; + + fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { + // let all_findings = input + // .par_iter() + // .map(|file| { + // let lints = self.lints.clone(); + // let mut local_findings = HashMap::new(); + + // // Create a new session for this file + // let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + // let arena = ast::Arena::new(); + + // // Enter the session context for this thread + // let _ = sess.enter(|| -> solar_interface::Result<()> { + // let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + + // let ast = + // parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + + // // Run all lints on the parsed AST and collect findings + // for mut lint in lints { + // let results = lint.lint(&ast); + // local_findings.entry(lint).or_insert_with(Vec::new).extend(results); + // } + + // Ok(()) + // }); + + // local_findings + // }) + // .collect::>>>(); + + todo!() + } +} From 664bdc38692f558d7a7f26b4bff52d9cdd3d26c8 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 02:00:30 -0500 Subject: [PATCH 031/107] fmt --- crates/lint/src/sol/mod.rs | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index bb5604f0abd1c..20a9d99ad4656 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,3 +1,8 @@ +pub mod gas; +pub mod high; +pub mod info; +pub mod med; + use std::path::PathBuf; use eyre::Error; @@ -10,31 +15,13 @@ use thiserror::Error; use crate::{Lint, Linter, LinterOutput, SourceLocation}; -pub mod gas; -pub mod high; -pub mod info; -pub mod med; - -#[derive(Debug, Hash)] -pub enum SolLint {} - -impl Lint for SolLint { - fn results(&self) -> Vec { - todo!() - } -} - #[derive(Debug, Clone)] pub struct SolidityLinter {} -#[derive(Error, Debug)] -pub enum SolLintError {} - impl Linter for SolidityLinter { - // TODO: update this to be a solar error - type LinterError = SolLintError; type Lint = SolLint; type Language = SolcLanguage; + type LinterError = SolLintError; fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { // let all_findings = input @@ -70,3 +57,15 @@ impl Linter for SolidityLinter { todo!() } } + +#[derive(Debug, Hash)] +pub enum SolLint {} + +impl Lint for SolLint { + fn results(&self) -> Vec { + todo!() + } +} + +#[derive(Error, Debug)] +pub enum SolLintError {} From e9fe6dcb665cd62d81d8eea90d2678404b1d8017 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 02:05:21 -0500 Subject: [PATCH 032/107] wip --- crates/lint/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 2c8f7c1c764cf..ab2ff7a07a502 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -135,6 +135,66 @@ pub struct SourceLocation { pub span: Span, } +// TODO: amend to display source location +// /// Tries to mimic Solidity's own error formatting. +// /// +// /// +// impl fmt::Display for Error { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// let mut short_msg = self.message.trim(); +// let fmtd_msg = self.formatted_message.as_deref().unwrap_or(""); + +// if short_msg.is_empty() { +// // if the message is empty, try to extract the first line from the formatted message +// if let Some(first_line) = fmtd_msg.lines().next() { +// // this is something like `ParserError: ` +// if let Some((_, s)) = first_line.split_once(':') { +// short_msg = s.trim_start(); +// } else { +// short_msg = first_line; +// } +// } +// } + +// // Error (XXXX): Error Message +// styled(f, self.severity.color().bold(), |f| self.fmt_severity(f))?; +// fmt_msg(f, short_msg)?; + +// let mut lines = fmtd_msg.lines(); + +// // skip the first line if it contains the same message as the one we just formatted, +// // unless it also contains a source location, in which case the entire error message is an +// // old style error message, like: +// // path/to/file:line:column: ErrorType: message +// if lines +// .clone() +// .next() +// .is_some_and(|l| l.contains(short_msg) && l.bytes().filter(|b| *b == b':').count() < 3) +// { +// let _ = lines.next(); +// } + +// // format the main source location +// fmt_source_location(f, &mut lines)?; + +// // format remaining lines as secondary locations +// while let Some(line) = lines.next() { +// f.write_str("\n")?; + +// if let Some((note, msg)) = line.split_once(':') { +// styled(f, Self::secondary_style(), |f| f.write_str(note))?; +// fmt_msg(f, msg)?; +// } else { +// f.write_str(line)?; +// } + +// fmt_source_location(f, &mut lines)?; +// } + +// Ok(()) +// } +// } + // impl Linter { // pub fn new(input: Vec) -> Self { // Self { input, lints: Lint::all(), description: false } From 18202ce9bac98cb6db60c47a6a3c407a1ec180b5 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 02:08:07 -0500 Subject: [PATCH 033/107] wip --- crates/lint/src/lib.rs | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index ab2ff7a07a502..444f628bcb4a9 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -56,6 +56,16 @@ where Self { linter, files: Vec::new(), severity: None, description: false } } + pub fn with_description(mut self, description: bool) -> Self { + self.description = description; + self + } + + pub fn with_severity(mut self, severity: Option>) -> Self { + self.severity = severity; + self + } + /// Lints the project. pub fn lint>( mut self, @@ -67,17 +77,6 @@ where std::process::exit(0); } - // // Taking is fine since we don't need these in `compile_with`. - // let files = std::mem::take(&mut self.files); - // self.compile_with(|| { - // let sources = if !files.is_empty() { - // Source::read_all(files)? - // } else { - // project.paths.read_input_files()? - // }; - - // }) - let sources = if !self.files.is_empty() { Source::read_all(self.files.clone())? } else { @@ -86,17 +85,7 @@ where let input = sources.into_iter().map(|(path, _)| path).collect::>(); - todo!() - } - - pub fn with_description(mut self, description: bool) -> Self { - self.description = description; - self - } - - pub fn with_severity(mut self, severity: Option>) -> Self { - self.severity = severity; - self + Ok(self.linter.lint(&input)?) } } From c19a441c6a8f23cd95a79fac33cedeef1c5a9116 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 14:36:05 -0500 Subject: [PATCH 034/107] refactor lints into SolLint enum --- crates/lint/src/lib.rs | 132 +------------------------ crates/lint/src/sol/gas.rs | 186 ++++++++++++++++++------------------ crates/lint/src/sol/high.rs | 25 ++--- crates/lint/src/sol/info.rs | 124 ++++++++++++------------ crates/lint/src/sol/med.rs | 108 ++++++++++----------- crates/lint/src/sol/mod.rs | 141 +++++++++++++++++++++++++-- 6 files changed, 360 insertions(+), 356 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 444f628bcb4a9..93f39f4ec923d 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -85,7 +85,9 @@ where let input = sources.into_iter().map(|(path, _)| path).collect::>(); - Ok(self.linter.lint(&input)?) + // Ok(self.linter.lint(&input)?) + + todo!() } } @@ -251,131 +253,3 @@ pub struct SourceLocation { // } // } // } - -// macro_rules! declare_lints { -// ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { -// #[derive(Debug, Clone, PartialEq, Eq)] -// pub enum Lint { -// $( -// $name($name), -// )* -// } - -// impl Lint { -// pub fn all() -> Vec { -// vec![ -// $( -// Lint::$name($name::new()), -// )* -// ] -// } - -// pub fn severity(&self) -> Severity { -// match self { -// $( -// Lint::$name(_) => $severity, -// )* -// } -// } - -// pub fn name(&self) -> &'static str { -// match self { -// $( -// Lint::$name(_) => $lint_name, -// )* -// } -// } - -// pub fn description(&self) -> &'static str { -// match self { -// $( -// Lint::$name(_) => $description, -// )* -// } -// } - -// /// Lint a source unit and return the findings -// pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { -// match self { -// $( -// Lint::$name(lint) => { -// lint.visit_source_unit(source_unit); -// lint.items.clone() -// }, -// )* -// } -// } -// } - -// impl<'ast> Visit<'ast> for Lint { -// fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { -// match self { -// $( -// Lint::$name(lint) => lint.visit_source_unit(source_unit), -// )* -// } -// } -// } - -// impl std::hash::Hash for Lint { -// fn hash(&self, state: &mut H) { -// self.name().hash(state); -// } -// } - -// $( -// #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -// pub struct $name { -// pub items: Vec, -// } - -// impl $name { -// pub fn new() -> Self { -// Self { items: Vec::new() } -// } - -// /// Returns the severity of the lint -// pub fn severity() -> Severity { -// $severity -// } - -// /// Returns the name of the lint -// pub fn name() -> &'static str { -// $lint_name -// } - -// /// Returns the description of the lint -// pub fn description() -> &'static str { -// $description -// } -// } -// )* -// }; -// } - -// declare_lints!( -// //High -// (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), -// (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), -// // Med -// (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), -// // Low -// // Info -// (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), -// (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), -// (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), -// (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), -// // Gas Optimizations -// (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), -// (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), -// (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), -// (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), -// (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), -// (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description"), -// ( -// AvoidUsingThis, -// Severity::Gas, -// "avoid-using-this", -// "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL." -// ), -// ); diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 66d4651645a50..00a776aedd8a2 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -1,93 +1,93 @@ -// use solar_ast::{ -// ast::{Expr, ExprKind}, -// visit::Visit, -// }; - -// use crate::{ -// AsmKeccak256, AvoidUsingThis, PackStorageVariables, PackStructs, UseConstantVariable, -// UseExternalVisibility, UseImmutableVariable, -// }; - -// impl<'ast> Visit<'ast> for AsmKeccak256 { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// if let ExprKind::Call(expr, _) = &expr.kind { -// if let ExprKind::Ident(ident) = &expr.kind { -// if ident.name.as_str() == "keccak256" { -// self.items.push(expr.span); -// } -// } -// } -// self.walk_expr(expr); -// } -// } - -// impl<'ast> Visit<'ast> for PackStorageVariables { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } - -// impl<'ast> Visit<'ast> for PackStructs { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } - -// impl<'ast> Visit<'ast> for UseConstantVariable { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } - -// impl<'ast> Visit<'ast> for UseImmutableVariable { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } - -// impl<'ast> Visit<'ast> for UseExternalVisibility { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } - -// impl<'ast> Visit<'ast> for AvoidUsingThis { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } - -// // TODO: avoid using `this` to read public variables - -// #[cfg(test)] -// mod test { -// use solar_ast::{ast, visit::Visit}; -// use solar_interface::{ColorChoice, Session}; -// use std::path::Path; - -// use crate::AsmKeccak256; - -// #[test] -// fn test_keccak256() -> eyre::Result<()> { -// let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - -// let _ = sess.enter(|| -> solar_interface::Result<()> { -// let arena = ast::Arena::new(); - -// let mut parser = -// solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol"))?; - -// // Parse the file. -// let ast = parser.parse_file().map_err(|e| e.emit())?; - -// let mut pattern = AsmKeccak256::default(); -// pattern.visit_source_unit(&ast); - -// assert_eq!(pattern.items.len(), 2); - -// Ok(()) -// }); - -// Ok(()) -// } -// } +use solar_ast::{ + ast::{Expr, ExprKind}, + visit::Visit, +}; + +use super::{ + AsmKeccak256, AvoidUsingThis, PackStorageVariables, PackStructs, UseConstantVariable, + UseExternalVisibility, UseImmutableVariable, +}; + +impl<'ast> Visit<'ast> for AsmKeccak256 { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + if let ExprKind::Call(expr, _) = &expr.kind { + if let ExprKind::Ident(ident) = &expr.kind { + if ident.name.as_str() == "keccak256" { + self.items.push(expr.span); + } + } + } + self.walk_expr(expr); + } +} + +impl<'ast> Visit<'ast> for PackStorageVariables { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for PackStructs { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for UseConstantVariable { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for UseImmutableVariable { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for UseExternalVisibility { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for AvoidUsingThis { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +// TODO: avoid using `this` to read public variables + +#[cfg(test)] +mod test { + use solar_ast::{ast, visit::Visit}; + use solar_interface::{ColorChoice, Session}; + use std::path::Path; + + use super::AsmKeccak256; + + #[test] + fn test_keccak256() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = + solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol"))?; + + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit())?; + + let mut pattern = AsmKeccak256::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.items.len(), 2); + + Ok(()) + }); + + Ok(()) + } +} diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high.rs index 6a43a33aff04c..49e08bf56cc75 100644 --- a/crates/lint/src/sol/high.rs +++ b/crates/lint/src/sol/high.rs @@ -1,14 +1,15 @@ -// use crate::{ArbitraryTransferFrom, IncorrectShift}; -// use solar_ast::{ast::Expr, visit::Visit}; +use solar_ast::{ast::Expr, visit::Visit}; -// impl<'ast> Visit<'ast> for IncorrectShift { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } +use super::{ArbitraryTransferFrom, IncorrectShift}; -// impl<'ast> Visit<'ast> for ArbitraryTransferFrom { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// todo!() -// } -// } +impl<'ast> Visit<'ast> for IncorrectShift { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} + +impl<'ast> Visit<'ast> for ArbitraryTransferFrom { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + todo!() + } +} diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 740951d9adb13..9235cf8e73cee 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -1,72 +1,72 @@ -// use regex::Regex; +use regex::Regex; -// use solar_ast::{ -// ast::{ItemStruct, VariableDefinition}, -// visit::Visit, -// }; +use solar_ast::{ + ast::{ItemStruct, VariableDefinition}, + visit::Visit, +}; -// use crate::{FunctionCamelCase, StructPascalCase, VariableCamelCase, VariableCapsCase}; +use super::{FunctionCamelCase, StructPascalCase, VariableCamelCase, VariableCapsCase}; -// impl<'ast> Visit<'ast> for VariableCamelCase { -// fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { -// if let Some(mutability) = var.mutability { -// if !mutability.is_constant() && !mutability.is_immutable() { -// if let Some(name) = var.name { -// if !is_camel_case(name.as_str()) { -// self.items.push(var.span); -// } -// } -// } -// } -// self.walk_variable_definition(var); -// } -// } +impl<'ast> Visit<'ast> for VariableCamelCase { + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + if let Some(mutability) = var.mutability { + if !mutability.is_constant() && !mutability.is_immutable() { + if let Some(name) = var.name { + if !is_camel_case(name.as_str()) { + self.items.push(var.span); + } + } + } + } + self.walk_variable_definition(var); + } +} -// impl<'ast> Visit<'ast> for VariableCapsCase { -// fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { -// if let Some(mutability) = var.mutability { -// if mutability.is_constant() || mutability.is_immutable() { -// if let Some(name) = var.name { -// if !is_caps_case(name.as_str()) { -// self.items.push(var.span); -// } -// } -// } -// } -// self.walk_variable_definition(var); -// } -// } +impl<'ast> Visit<'ast> for VariableCapsCase { + fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { + if let Some(mutability) = var.mutability { + if mutability.is_constant() || mutability.is_immutable() { + if let Some(name) = var.name { + if !is_caps_case(name.as_str()) { + self.items.push(var.span); + } + } + } + } + self.walk_variable_definition(var); + } +} -// impl<'ast> Visit<'ast> for StructPascalCase { -// fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { -// if !is_pascal_case(strukt.name.as_str()) { -// self.items.push(strukt.name.span); -// } +impl<'ast> Visit<'ast> for StructPascalCase { + fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { + if !is_pascal_case(strukt.name.as_str()) { + self.items.push(strukt.name.span); + } -// self.walk_item_struct(strukt); -// } -// } + self.walk_item_struct(strukt); + } +} -// impl Visit<'_> for FunctionCamelCase { -// fn visit_function_header(&mut self, header: &'_ solar_ast::ast::FunctionHeader<'_>) { -// todo!() -// } -// } +impl Visit<'_> for FunctionCamelCase { + fn visit_function_header(&mut self, header: &'_ solar_ast::ast::FunctionHeader<'_>) { + todo!() + } +} -// // Check if a string is camelCase -// pub fn is_camel_case(s: &str) -> bool { -// let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); -// re.is_match(s) && s.chars().any(|c| c.is_uppercase()) -// } +// Check if a string is camelCase +pub fn is_camel_case(s: &str) -> bool { + let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); + re.is_match(s) && s.chars().any(|c| c.is_uppercase()) +} -// // Check if a string is PascalCase -// pub fn is_pascal_case(s: &str) -> bool { -// let re = Regex::new(r"^[A-Z0-9][a-zA-Z0-9]*$").unwrap(); -// re.is_match(s) -// } +// Check if a string is PascalCase +pub fn is_pascal_case(s: &str) -> bool { + let re = Regex::new(r"^[A-Z0-9][a-zA-Z0-9]*$").unwrap(); + re.is_match(s) +} -// // Check if a string is SCREAMING_SNAKE_CASE -// pub fn is_caps_case(s: &str) -> bool { -// let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); -// re.is_match(s) && s.contains('_') -// } +// Check if a string is SCREAMING_SNAKE_CASE +pub fn is_caps_case(s: &str) -> bool { + let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); + re.is_match(s) && s.contains('_') +} diff --git a/crates/lint/src/sol/med.rs b/crates/lint/src/sol/med.rs index bfd5902a82330..3d4c0e378eef3 100644 --- a/crates/lint/src/sol/med.rs +++ b/crates/lint/src/sol/med.rs @@ -1,68 +1,68 @@ -// use solar_ast::{ -// ast::{BinOp, BinOpKind, Expr, ExprKind}, -// visit::Visit, -// }; +use solar_ast::{ + ast::{BinOp, BinOpKind, Expr, ExprKind}, + visit::Visit, +}; -// use crate::DivideBeforeMultiply; +use super::DivideBeforeMultiply; -// impl<'ast> Visit<'ast> for DivideBeforeMultiply { -// fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { -// if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { -// if contains_division(left_expr) { -// self.items.push(expr.span); -// } -// } +impl<'ast> Visit<'ast> for DivideBeforeMultiply { + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { + if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { + if contains_division(left_expr) { + self.items.push(expr.span); + } + } -// self.walk_expr(expr); -// } -// } + self.walk_expr(expr); + } +} -// fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { -// match &expr.kind { -// ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, -// ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { -// if let Some(inner_expr) = opt_expr { -// contains_division(inner_expr) -// } else { -// false -// } -// }), -// _ => false, -// } -// } +fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { + match &expr.kind { + ExprKind::Binary(_, BinOp { kind: BinOpKind::Div, .. }, _) => true, + ExprKind::Tuple(inner_exprs) => inner_exprs.iter().any(|opt_expr| { + if let Some(inner_expr) = opt_expr { + contains_division(inner_expr) + } else { + false + } + }), + _ => false, + } +} -// #[cfg(test)] -// mod test { -// use solar_ast::{ast, visit::Visit}; -// use solar_interface::{ColorChoice, Session}; -// use std::path::Path; +#[cfg(test)] +mod test { + use solar_ast::{ast, visit::Visit}; + use solar_interface::{ColorChoice, Session}; + use std::path::Path; -// use crate::DivideBeforeMultiply; + use super::DivideBeforeMultiply; -// #[test] -// fn test_divide_before_multiply() -> eyre::Result<()> { -// let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + #[test] + fn test_divide_before_multiply() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); -// let _ = sess.enter(|| -> solar_interface::Result<()> { -// let arena = ast::Arena::new(); + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); -// let mut parser = solar_parse::Parser::from_file( -// &sess, -// &arena, -// Path::new("testdata/DivideBeforeMultiply.sol"), -// )?; + let mut parser = solar_parse::Parser::from_file( + &sess, + &arena, + Path::new("testdata/DivideBeforeMultiply.sol"), + )?; -// // Parse the file. -// let ast = parser.parse_file().map_err(|e| e.emit())?; + // Parse the file. + let ast = parser.parse_file().map_err(|e| e.emit())?; -// let mut pattern = DivideBeforeMultiply::default(); -// pattern.visit_source_unit(&ast); + let mut pattern = DivideBeforeMultiply::default(); + pattern.visit_source_unit(&ast); -// assert_eq!(pattern.items.len(), 6); + assert_eq!(pattern.items.len(), 6); -// Ok(()) -// }); + Ok(()) + }); -// Ok(()) -// } -// } + Ok(()) + } +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 20a9d99ad4656..8623f11582e29 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -3,17 +3,21 @@ pub mod high; pub mod info; pub mod med; -use std::path::PathBuf; +use std::{ + hash::{Hash, Hasher}, + path::PathBuf, +}; use eyre::Error; use foundry_compilers::solc::SolcLanguage; +use solar_ast::{ast::SourceUnit, visit::Visit}; use solar_interface::{ diagnostics::{DiagnosticBuilder, ErrorGuaranteed}, - ColorChoice, Session, + ColorChoice, Session, Span, }; use thiserror::Error; -use crate::{Lint, Linter, LinterOutput, SourceLocation}; +use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; #[derive(Debug, Clone)] pub struct SolidityLinter {} @@ -58,9 +62,6 @@ impl Linter for SolidityLinter { } } -#[derive(Debug, Hash)] -pub enum SolLint {} - impl Lint for SolLint { fn results(&self) -> Vec { todo!() @@ -69,3 +70,131 @@ impl Lint for SolLint { #[derive(Error, Debug)] pub enum SolLintError {} + +macro_rules! declare_sol_lints { + ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum SolLint { + $( + $name($name), + )* + } + + impl SolLint { + pub fn all() -> Vec { + vec![ + $( + SolLint::$name($name::new()), + )* + ] + } + + pub fn severity(&self) -> Severity { + match self { + $( + SolLint::$name(_) => $severity, + )* + } + } + + pub fn name(&self) -> &'static str { + match self { + $( + SolLint::$name(_) => $lint_name, + )* + } + } + + pub fn description(&self) -> &'static str { + match self { + $( + SolLint::$name(_) => $description, + )* + } + } + + /// Lint a source unit and return the findings + pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { + match self { + $( + SolLint::$name(lint) => { + lint.visit_source_unit(source_unit); + lint.items.clone() + }, + )* + } + } + } + + impl<'ast> Visit<'ast> for SolLint { + fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { + match self { + $( + SolLint::$name(lint) => lint.visit_source_unit(source_unit), + )* + } + } + } + + impl Hash for SolLint { + fn hash(&self, state: &mut H) { + self.name().hash(state); + } + } + + $( + #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] + pub struct $name { + pub items: Vec, + } + + impl $name { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + /// Returns the severity of the lint + pub fn severity() -> Severity { + $severity + } + + /// Returns the name of the lint + pub fn name() -> &'static str { + $lint_name + } + + /// Returns the description of the lint + pub fn description() -> &'static str { + $description + } + } + )* + }; +} + +declare_sol_lints!( + //High + (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), + (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), + // Med + (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), + // Low + // Info + (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), + (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), + (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), + (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), + // Gas Optimizations + (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), + (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), + (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), + (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), + (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), + (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description"), + ( + AvoidUsingThis, + Severity::Gas, + "avoid-using-this", + "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL." + ), +); From d0f552e309e351cbfa0efb62bde9cd1428e75c96 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 14:51:19 -0500 Subject: [PATCH 035/107] update lint trait --- crates/lint/src/lib.rs | 114 +++++++----------------------------- crates/lint/src/sol/gas.rs | 4 +- crates/lint/src/sol/info.rs | 6 +- crates/lint/src/sol/med.rs | 4 +- crates/lint/src/sol/mod.rs | 55 ++++++++--------- 5 files changed, 54 insertions(+), 129 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 93f39f4ec923d..52225302a0713 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -16,26 +16,7 @@ use std::{ }; use clap::ValueEnum; -use solar_ast::{ - ast::{self, SourceUnit, Span}, - interface::{ColorChoice, Session}, - visit::Visit, -}; - -#[derive(Clone, Debug, ValueEnum)] -pub enum OutputFormat { - Json, - Markdown, -} - -#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] -pub enum Severity { - High, - Med, - Low, - Info, - Gas, -} +use solar_ast::ast::{self, SourceUnit, Span}; pub struct ProjectLinter where @@ -68,7 +49,7 @@ where /// Lints the project. pub fn lint>( - mut self, + self, project: &Project, ) -> eyre::Result> { if !project.paths.has_input_files() && self.files.is_empty() { @@ -85,9 +66,7 @@ where let input = sources.into_iter().map(|(path, _)| path).collect::>(); - // Ok(self.linter.lint(&input)?) - - todo!() + Ok(self.linter.lint(&input).expect("TODO: handle error")) } } @@ -117,11 +96,28 @@ pub struct LinterOutput { } pub trait Lint: Hash { - fn results(&self) -> Vec; + fn name(&self) -> &'static str; + fn description(&self) -> &'static str; + fn severity(&self) -> Severity; +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum OutputFormat { + Json, + Markdown, } +#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum Severity { + High, + Med, + Low, + Info, + Gas, +} #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SourceLocation { + // TODO: should this be path buf? pub file: String, pub span: Span, } @@ -185,71 +181,3 @@ pub struct SourceLocation { // Ok(()) // } // } - -// impl Linter { -// pub fn new(input: Vec) -> Self { -// Self { input, lints: Lint::all(), description: false } -// } - -// pub fn with_severity(mut self, severity: Option>) -> Self { -// if let Some(severity) = severity { -// self.lints.retain(|lint| severity.contains(&lint.severity())); -// } -// self -// } - -// pub fn with_description(mut self, description: bool) -> Self { -// self.description = description; -// self -// } - -// pub fn lint(self) { -// let all_findings = self -// .input -// .par_iter() -// .map(|file| { -// let lints = self.lints.clone(); -// let mut local_findings = HashMap::new(); - -// // Create a new session for this file -// let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); -// let arena = ast::Arena::new(); - -// // Enter the session context for this thread -// let _ = sess.enter(|| -> solar_interface::Result<()> { -// let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - -// let ast = -// parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - -// // Run all lints on the parsed AST and collect findings -// for mut lint in lints { -// let results = lint.lint(&ast); -// local_findings.entry(lint).or_insert_with(Vec::new).extend(results); -// } - -// Ok(()) -// }); - -// local_findings -// }) -// .collect::>>>(); - -// let mut aggregated_findings = HashMap::new(); -// for file_findings in all_findings { -// for (lint, results) in file_findings { -// aggregated_findings.entry(lint).or_insert_with(Vec::new).extend(results); -// } -// } - -// // TODO: make the output nicer -// for finding in aggregated_findings { -// let (lint, results) = finding; -// let _description = if self.description { lint.description() } else { "" }; - -// for _result in results { -// // TODO: display the finding -// } -// } -// } -// } diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 00a776aedd8a2..5bae74a4baf59 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -13,7 +13,7 @@ impl<'ast> Visit<'ast> for AsmKeccak256 { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { if ident.name.as_str() == "keccak256" { - self.items.push(expr.span); + self.results.push(expr.span); } } } @@ -83,7 +83,7 @@ mod test { let mut pattern = AsmKeccak256::default(); pattern.visit_source_unit(&ast); - assert_eq!(pattern.items.len(), 2); + assert_eq!(pattern.results.len(), 2); Ok(()) }); diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 9235cf8e73cee..9c0bf47699ee2 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -13,7 +13,7 @@ impl<'ast> Visit<'ast> for VariableCamelCase { if !mutability.is_constant() && !mutability.is_immutable() { if let Some(name) = var.name { if !is_camel_case(name.as_str()) { - self.items.push(var.span); + self.results.push(var.span); } } } @@ -28,7 +28,7 @@ impl<'ast> Visit<'ast> for VariableCapsCase { if mutability.is_constant() || mutability.is_immutable() { if let Some(name) = var.name { if !is_caps_case(name.as_str()) { - self.items.push(var.span); + self.results.push(var.span); } } } @@ -40,7 +40,7 @@ impl<'ast> Visit<'ast> for VariableCapsCase { impl<'ast> Visit<'ast> for StructPascalCase { fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { if !is_pascal_case(strukt.name.as_str()) { - self.items.push(strukt.name.span); + self.results.push(strukt.name.span); } self.walk_item_struct(strukt); diff --git a/crates/lint/src/sol/med.rs b/crates/lint/src/sol/med.rs index 3d4c0e378eef3..02314699e1c9b 100644 --- a/crates/lint/src/sol/med.rs +++ b/crates/lint/src/sol/med.rs @@ -9,7 +9,7 @@ impl<'ast> Visit<'ast> for DivideBeforeMultiply { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { if contains_division(left_expr) { - self.items.push(expr.span); + self.results.push(expr.span); } } @@ -58,7 +58,7 @@ mod test { let mut pattern = DivideBeforeMultiply::default(); pattern.visit_source_unit(&ast); - assert_eq!(pattern.items.len(), 6); + assert_eq!(pattern.results.len(), 6); Ok(()) }); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 8623f11582e29..b2bef347949ce 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -62,12 +62,6 @@ impl Linter for SolidityLinter { } } -impl Lint for SolLint { - fn results(&self) -> Vec { - todo!() - } -} - #[derive(Error, Debug)] pub enum SolLintError {} @@ -89,68 +83,71 @@ macro_rules! declare_sol_lints { ] } - pub fn severity(&self) -> Severity { + /// Lint a source unit and return the findings + pub fn lint(&mut self, source_unit: &SourceUnit<'_>) { match self { $( - SolLint::$name(_) => $severity, + SolLint::$name(lint) => { + lint.visit_source_unit(source_unit); + }, )* } } + } - pub fn name(&self) -> &'static str { + impl<'ast> Visit<'ast> for SolLint { + fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { match self { $( - SolLint::$name(_) => $lint_name, + SolLint::$name(lint) => lint.visit_source_unit(source_unit), )* } } + } + + impl Hash for SolLint { + fn hash(&self, state: &mut H) { + self.name().hash(state); + } + } - pub fn description(&self) -> &'static str { + impl Lint for SolLint { + fn name(&self) -> &'static str { match self { $( - SolLint::$name(_) => $description, + SolLint::$name(_) => $lint_name, )* } } - /// Lint a source unit and return the findings - pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { + fn description(&self) -> &'static str { match self { $( - SolLint::$name(lint) => { - lint.visit_source_unit(source_unit); - lint.items.clone() - }, + SolLint::$name(_) => $description, )* } } - } - impl<'ast> Visit<'ast> for SolLint { - fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) { + fn severity(&self) -> Severity { match self { $( - SolLint::$name(lint) => lint.visit_source_unit(source_unit), + SolLint::$name(_) => $severity, )* } } } - impl Hash for SolLint { - fn hash(&self, state: &mut H) { - self.name().hash(state); - } - } $( #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct $name { - pub items: Vec, + // TODO: make source location and option + pub results: Vec, } impl $name { pub fn new() -> Self { - Self { items: Vec::new() } + Self { results: Vec::new() } } /// Returns the severity of the lint From e88bc8719cb74f93ecb637600dd2d51a55b11c91 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 14:56:20 -0500 Subject: [PATCH 036/107] wip --- crates/lint/src/sol/mod.rs | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index b2bef347949ce..0cdd9157787b6 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -4,13 +4,18 @@ pub mod info; pub mod med; use std::{ + collections::BTreeMap, hash::{Hash, Hasher}, path::PathBuf, }; use eyre::Error; use foundry_compilers::solc::SolcLanguage; -use solar_ast::{ast::SourceUnit, visit::Visit}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use solar_ast::{ + ast::{Arena, SourceUnit}, + visit::Visit, +}; use solar_interface::{ diagnostics::{DiagnosticBuilder, ErrorGuaranteed}, ColorChoice, Session, Span, @@ -28,35 +33,30 @@ impl Linter for SolidityLinter { type LinterError = SolLintError; fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { - // let all_findings = input - // .par_iter() - // .map(|file| { - // let lints = self.lints.clone(); - // let mut local_findings = HashMap::new(); - - // // Create a new session for this file - // let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - // let arena = ast::Arena::new(); - - // // Enter the session context for this thread - // let _ = sess.enter(|| -> solar_interface::Result<()> { - // let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - - // let ast = - // parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - - // // Run all lints on the parsed AST and collect findings - // for mut lint in lints { - // let results = lint.lint(&ast); - // local_findings.entry(lint).or_insert_with(Vec::new).extend(results); - // } - - // Ok(()) - // }); - - // local_findings - // }) - // .collect::>>>(); + let all_findings = input.par_iter().map(|file| { + // NOTE: use all solidity lints for now but this should be configurable via SolidityLinter + let mut lints = SolLint::all(); + + // Initialize session and parsing environment + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + let arena = Arena::new(); + let mut local_findings = BTreeMap::new(); + + // Enter the session context for this thread + let _ = sess.enter(|| -> solar_interface::Result<()> { + let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + + let ast = parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + + // Run all lints on the parsed AST and collect findings + for mut lint in lints { + let results = lint.lint(&ast); + local_findings.entry(lint).or_insert_with(Vec::new).extend(results); + } + + Ok(()) + }); + }); todo!() } From 1203d32fc1efe748a871bf1af6db2a147c2ae1f9 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 14:57:07 -0500 Subject: [PATCH 037/107] wip --- crates/lint/src/lib.rs | 4 ++-- crates/lint/src/sol/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 52225302a0713..ecc9947cae705 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -75,12 +75,12 @@ where /// The main linter abstraction trait pub trait Linter: Send + Sync + Clone { + /// Enum of languages supported by the linter. + type Language: Language; // TODO: Add docs. This represents linter settings. (ex. Default, OP Stack, etc. // type Settings: LinterSettings; type Lint: Lint; type LinterError: Error; - /// Enum of languages supported by the linter. - type Language: Language; /// Main entrypoint for the linter. fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 0cdd9157787b6..4f0a1ec4b8764 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -28,8 +28,8 @@ use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; pub struct SolidityLinter {} impl Linter for SolidityLinter { - type Lint = SolLint; type Language = SolcLanguage; + type Lint = SolLint; type LinterError = SolLintError; fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { From 01bb6dacc7003a01b4658bad1066039f1cc35c3c Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sat, 28 Dec 2024 14:59:03 -0500 Subject: [PATCH 038/107] wip --- crates/lint/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index ecc9947cae705..7859c1b45b422 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -107,6 +107,7 @@ pub enum OutputFormat { Markdown, } +// TODO: impl color for severity #[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum Severity { High, From 9d3abf0f992203b67895d2c186a97a7f8078a7a8 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 00:45:15 -0500 Subject: [PATCH 039/107] wip --- crates/lint/src/lib.rs | 32 +++++++++++++++++++++++++++--- crates/lint/src/sol/mod.rs | 40 +++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 7859c1b45b422..f7a560eb20ab6 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -12,6 +12,7 @@ use std::{ error::Error, hash::{Hash, Hasher}, marker::PhantomData, + ops::{Deref, DerefMut}, path::PathBuf, }; @@ -91,8 +92,27 @@ pub trait LinterSettings { fn lints() -> Vec; } -pub struct LinterOutput { - pub results: BTreeMap>, +pub struct LinterOutput(pub BTreeMap>); + +impl LinterOutput { + // Optional: You can still provide a `new` method for convenience + pub fn new() -> Self { + LinterOutput(BTreeMap::new()) + } +} + +impl Deref for LinterOutput { + type Target = BTreeMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for LinterOutput { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } } pub trait Lint: Hash { @@ -119,10 +139,16 @@ pub enum Severity { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SourceLocation { // TODO: should this be path buf? - pub file: String, + pub file: PathBuf, pub span: Span, } +impl SourceLocation { + pub fn new(file: PathBuf, span: Span) -> Self { + Self { file, span } + } +} + // TODO: amend to display source location // /// Tries to mimic Solidity's own error formatting. // /// diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 4f0a1ec4b8764..ec8b4918f5727 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -4,14 +4,14 @@ pub mod info; pub mod med; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, hash::{Hash, Hasher}, path::PathBuf, }; use eyre::Error; use foundry_compilers::solc::SolcLanguage; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use solar_ast::{ ast::{Arena, SourceUnit}, visit::Visit, @@ -33,28 +33,33 @@ impl Linter for SolidityLinter { type LinterError = SolLintError; fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { - let all_findings = input.par_iter().map(|file| { + let all_findings = input.into_par_iter().map(|file| { // NOTE: use all solidity lints for now but this should be configurable via SolidityLinter - let mut lints = SolLint::all(); + let lints = SolLint::all(); // Initialize session and parsing environment let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); let arena = Arena::new(); - let mut local_findings = BTreeMap::new(); // Enter the session context for this thread - let _ = sess.enter(|| -> solar_interface::Result<()> { + let _ = sess.enter(|| -> solar_interface::Result> { let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - let ast = parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + let mut local_findings = LinterOutput::new(); // Run all lints on the parsed AST and collect findings - for mut lint in lints { - let results = lint.lint(&ast); - local_findings.entry(lint).or_insert_with(Vec::new).extend(results); + for mut lint in lints.into_iter() { + if let Some(findings) = lint.lint(&ast) { + let findings = findings + .into_iter() + .map(|span| SourceLocation::new(file.to_owned(), span)) + .collect::>(); + + local_findings.insert(lint, findings); + } } - Ok(()) + Ok(local_findings) }); }); @@ -67,7 +72,9 @@ pub enum SolLintError {} macro_rules! declare_sol_lints { ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { - #[derive(Debug, Clone, PartialEq, Eq)] + + // TODO: ord based on severity + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SolLint { $( $name($name), @@ -84,11 +91,12 @@ macro_rules! declare_sol_lints { } /// Lint a source unit and return the findings - pub fn lint(&mut self, source_unit: &SourceUnit<'_>) { + pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Option> { match self { $( SolLint::$name(lint) => { lint.visit_source_unit(source_unit); + lint.results.clone() }, )* } @@ -139,15 +147,15 @@ macro_rules! declare_sol_lints { $( - #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] + #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct $name { // TODO: make source location and option - pub results: Vec, + pub results: Option>, } impl $name { pub fn new() -> Self { - Self { results: Vec::new() } + Self { results: None } } /// Returns the severity of the lint From e8b53ca6a10fb6271faedaebe95ba85d1e7d7f2f Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 00:48:42 -0500 Subject: [PATCH 040/107] wip --- crates/lint/src/sol/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index ec8b4918f5727..2e51eb58209e0 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -49,13 +49,14 @@ impl Linter for SolidityLinter { let mut local_findings = LinterOutput::new(); // Run all lints on the parsed AST and collect findings for mut lint in lints.into_iter() { - if let Some(findings) = lint.lint(&ast) { - let findings = findings + let findings = lint.lint(&ast); + if !findings.is_empty() { + let source_locations = findings .into_iter() .map(|span| SourceLocation::new(file.to_owned(), span)) .collect::>(); - local_findings.insert(lint, findings); + local_findings.insert(lint, source_locations); } } @@ -91,7 +92,7 @@ macro_rules! declare_sol_lints { } /// Lint a source unit and return the findings - pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Option> { + pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { match self { $( SolLint::$name(lint) => { @@ -149,13 +150,12 @@ macro_rules! declare_sol_lints { $( #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct $name { - // TODO: make source location and option - pub results: Option>, + pub results: Vec, } impl $name { pub fn new() -> Self { - Self { results: None } + Self { results: Vec::new() } } /// Returns the severity of the lint From 5c5343d4d55b060826e7019e0c680776aab91752 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 01:14:43 -0500 Subject: [PATCH 041/107] update lint --- crates/lint/src/lib.rs | 10 ++++- crates/lint/src/sol/mod.rs | 76 +++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index f7a560eb20ab6..65dd0acf8d73e 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -80,7 +80,7 @@ pub trait Linter: Send + Sync + Clone { type Language: Language; // TODO: Add docs. This represents linter settings. (ex. Default, OP Stack, etc. // type Settings: LinterSettings; - type Lint: Lint; + type Lint: Lint + Ord; type LinterError: Error; /// Main entrypoint for the linter. @@ -115,6 +115,14 @@ impl DerefMut for LinterOutput { } } +impl Extend<(L::Lint, Vec)> for LinterOutput { + fn extend)>>(&mut self, iter: T) { + for (lint, findings) in iter { + self.0.entry(lint).or_insert_with(Vec::new).extend(findings); + } + } +} + pub trait Lint: Hash { fn name(&self) -> &'static str; fn description(&self) -> &'static str; diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 2e51eb58209e0..d83f4fc18719b 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -33,38 +33,48 @@ impl Linter for SolidityLinter { type LinterError = SolLintError; fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { - let all_findings = input.into_par_iter().map(|file| { - // NOTE: use all solidity lints for now but this should be configurable via SolidityLinter - let lints = SolLint::all(); - - // Initialize session and parsing environment - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - let arena = Arena::new(); - - // Enter the session context for this thread - let _ = sess.enter(|| -> solar_interface::Result> { - let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - let ast = parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - - let mut local_findings = LinterOutput::new(); - // Run all lints on the parsed AST and collect findings - for mut lint in lints.into_iter() { - let findings = lint.lint(&ast); - if !findings.is_empty() { - let source_locations = findings - .into_iter() - .map(|span| SourceLocation::new(file.to_owned(), span)) - .collect::>(); - - local_findings.insert(lint, source_locations); + let all_findings = input + .into_par_iter() + .map(|file| { + // NOTE: use all solidity lints for now but this should be configurable via SolidityLinter + let mut lints = SolLint::all(); + + // Initialize session and parsing environment + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + let arena = Arena::new(); + + // Enter the session context for this thread + let _ = sess.enter(|| -> solar_interface::Result<()> { + let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + let ast = + parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + + // Run all lints on the parsed AST and collect findings + for lint in lints.iter_mut() { + lint.lint(&ast); } - } - Ok(local_findings) - }); - }); + Ok(()) + }); + + (file.to_owned(), lints) + }) + .collect::)>>(); + + let mut output = LinterOutput::new(); + for (file, lints) in all_findings { + for lint in lints { + let source_locations = lint + .results() + .iter() + .map(|span| SourceLocation::new(file.clone(), *span)) + .collect::>(); + + output.insert(lint, source_locations); + } + } - todo!() + Ok(output) } } @@ -91,6 +101,14 @@ macro_rules! declare_sol_lints { ] } + pub fn results(&self) -> &[Span] { + match self { + $( + SolLint::$name(lint) => &lint.results, + )* + } + } + /// Lint a source unit and return the findings pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { match self { From 919f3b1c403d105a2882780240f81c801d7bae27 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 01:54:06 -0500 Subject: [PATCH 042/107] update forge lint to use ProjectLinter --- crates/forge/bin/cmd/lint.rs | 51 +++++++----------------------------- crates/lint/src/lib.rs | 34 +++++++++++++++++++++--- crates/lint/src/sol/mod.rs | 19 +++++++------- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 7496a9c4a7057..94b7b827f8017 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,7 +1,9 @@ use clap::{Parser, ValueHint}; use eyre::{bail, Result}; -use forge_lint::{Linter, OutputFormat, Severity}; +use forge_lint::sol::SolidityLinter; +use forge_lint::{Linter, OutputFormat, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; +use foundry_common::shell; use foundry_compilers::utils::{source_files_iter, SOLC_EXTENSIONS}; use foundry_config::filter::expand_globs; use foundry_config::impl_figment_convert_basic; @@ -52,50 +54,17 @@ impl_figment_convert_basic!(LintArgs); impl LintArgs { pub fn run(self) -> Result<()> { let config = self.try_load_config_emit_warnings()?; - - // Set up the project. let project = config.project()?; - let root = if let Some(root) = &self.root { root } else { &config.root }; - - // Expand ignore globs and canonicalize paths - let mut ignored = expand_globs(&root, config.fmt.ignore.iter())? - .iter() - .flat_map(foundry_common::fs::canonicalize_path) - .collect::>(); - - // Add explicitly excluded paths to the ignored set - if let Some(exclude_paths) = &self.exclude { - ignored.extend(exclude_paths.iter().flat_map(foundry_common::fs::canonicalize_path)); - } - - let entries = fs::read_dir(root).unwrap(); - println!("Files in directory: {}", root.display()); - for entry in entries { - let entry = entry.unwrap(); - let path = entry.path(); - println!("{}", path.display()); - } - - let mut input: Vec = if let Some(include_paths) = &self.include { - include_paths.iter().filter(|path| path.exists()).cloned().collect() - } else { - source_files_iter(&root, SOLC_EXTENSIONS) - .filter(|p| !(ignored.contains(p) || ignored.contains(&root.join(p)))) - .collect() - }; - - input.retain(|path| !ignored.contains(path)); + // TODO: Update this to infer the linter from the project, just hard coding to solidity for now + let linter = SolidityLinter::new(); + let output = ProjectLinter::new(linter).lint(&project)?; - if input.is_empty() { - bail!("No source files found in path"); - } + // let format_json = shell::is_json(); - // TODO: maybe compile and lint on the aggreagted compiler output? - Linter::new(input) - .with_severity(self.severity) - .with_description(self.with_description) - .lint(); + // if format_json && !self.names && !self.sizes { + // sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?; + // } Ok(()) } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 65dd0acf8d73e..c618419549b3a 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -7,6 +7,7 @@ use foundry_compilers::{ }; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use sol::SolidityLinter; use std::{ collections::{BTreeMap, HashMap}, error::Error, @@ -51,11 +52,37 @@ where /// Lints the project. pub fn lint>( self, - project: &Project, + mut project: &Project, ) -> eyre::Result> { + // TODO: infer linter from project + + // // Expand ignore globs and canonicalize paths + // let mut ignored = expand_globs(&root, config.fmt.ignore.iter())? + // .iter() + // .flat_map(foundry_common::fs::canonicalize_path) + // .collect::>(); + + // // Add explicitly excluded paths to the ignored set + // if let Some(exclude_paths) = &self.exclude { + // ignored.extend(exclude_paths.iter().flat_map(foundry_common::fs::canonicalize_path)); + // } + + // let mut input: Vec = if let Some(include_paths) = &self.include { + // include_paths.iter().filter(|path| path.exists()).cloned().collect() + // } else { + // source_files_iter(&root, SOLC_EXTENSIONS) + // .filter(|p| !(ignored.contains(p) || ignored.contains(&root.join(p)))) + // .collect() + // }; + + // input.retain(|path| !ignored.contains(path)); + + // if input.is_empty() { + // bail!("No source files found in path"); + // } + if !project.paths.has_input_files() && self.files.is_empty() { - sh_println!("Nothing to compile")?; - // nothing to do here + sh_println!("Nothing to lint")?; std::process::exit(0); } @@ -146,7 +173,6 @@ pub enum Severity { } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SourceLocation { - // TODO: should this be path buf? pub file: PathBuf, pub span: Span, } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index d83f4fc18719b..c77a54f36ec4c 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -4,28 +4,29 @@ pub mod info; pub mod med; use std::{ - collections::{BTreeMap, HashMap}, hash::{Hash, Hasher}, path::PathBuf, }; -use eyre::Error; use foundry_compilers::solc::SolcLanguage; -use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{ ast::{Arena, SourceUnit}, visit::Visit, }; -use solar_interface::{ - diagnostics::{DiagnosticBuilder, ErrorGuaranteed}, - ColorChoice, Session, Span, -}; +use solar_interface::{ColorChoice, Session, Span}; use thiserror::Error; use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; -#[derive(Debug, Clone)] -pub struct SolidityLinter {} +#[derive(Debug, Clone, Default)] +pub struct SolidityLinter; + +impl SolidityLinter { + pub fn new() -> Self { + Self::default() + } +} impl Linter for SolidityLinter { type Language = SolcLanguage; From f02c9679e2fb83f6c6b902068ef0d3477f08be3c Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 01:56:27 -0500 Subject: [PATCH 043/107] wip --- crates/forge/bin/cmd/lint.rs | 9 ++------- crates/lint/src/lib.rs | 4 ---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 94b7b827f8017..bea78f3bb2c3b 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,14 +1,9 @@ use clap::{Parser, ValueHint}; -use eyre::{bail, Result}; +use eyre::Result; use forge_lint::sol::SolidityLinter; -use forge_lint::{Linter, OutputFormat, ProjectLinter, Severity}; +use forge_lint::{OutputFormat, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; -use foundry_common::shell; -use foundry_compilers::utils::{source_files_iter, SOLC_EXTENSIONS}; -use foundry_config::filter::expand_globs; use foundry_config::impl_figment_convert_basic; -use std::collections::HashSet; -use std::fs; use std::path::PathBuf; /// CLI arguments for `forge lint`. diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index c618419549b3a..bcc20256ec84c 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -183,10 +183,6 @@ impl SourceLocation { } } -// TODO: amend to display source location -// /// Tries to mimic Solidity's own error formatting. -// /// -// /// // impl fmt::Display for Error { // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // let mut short_msg = self.message.trim(); From e37043c3489c6198f77ad7be08f2e05a8e175b6d Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 02:09:29 -0500 Subject: [PATCH 044/107] include/exclude files from linting --- crates/forge/bin/cmd/lint.rs | 39 +++++++++++++++++++++++++--- crates/lint/src/lib.rs | 49 ++---------------------------------- 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index bea78f3bb2c3b..2455c5fde774a 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -3,7 +3,9 @@ use eyre::Result; use forge_lint::sol::SolidityLinter; use forge_lint::{OutputFormat, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; +use foundry_compilers::artifacts::Source; use foundry_config::impl_figment_convert_basic; +use std::collections::HashSet; use std::path::PathBuf; /// CLI arguments for `forge lint`. @@ -51,10 +53,41 @@ impl LintArgs { let config = self.try_load_config_emit_warnings()?; let project = config.project()?; - // TODO: Update this to infer the linter from the project, just hard coding to solidity for now - let linter = SolidityLinter::new(); - let output = ProjectLinter::new(linter).lint(&project)?; + // Get all source files from the project + let mut sources = + project.paths.read_input_files()?.keys().cloned().collect::>(); + // Add included paths to sources + if let Some(include_paths) = &self.include { + let included = include_paths + .iter() + .filter(|path| sources.contains(path)) + .cloned() + .collect::>(); + + sources = included; + } + + // Remove excluded files from sources + if let Some(exclude_paths) = &self.exclude { + let excluded = exclude_paths.iter().cloned().collect::>(); + sources.retain(|path| !excluded.contains(path)); + } + + if sources.is_empty() { + sh_println!("Nothing to lint")?; + std::process::exit(0); + } + + let linter = if project.compiler.solc.is_some() { + SolidityLinter::new() + } else { + todo!("Linting not supported for this language"); + }; + + let output = ProjectLinter::new(linter).lint(&sources)?; + + // TODO: display output // let format_json = shell::is_json(); // if format_json && !self.names && !self.sizes { diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index bcc20256ec84c..1fd2b09d8b959 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -25,8 +25,6 @@ where L: Linter, { pub linter: L, - /// Extra files to include, that are not necessarily in the project's source dir. - pub files: Vec, pub severity: Option>, pub description: bool, } @@ -36,7 +34,7 @@ where L: Linter, { pub fn new(linter: L) -> Self { - Self { linter, files: Vec::new(), severity: None, description: false } + Self { linter, severity: None, description: false } } pub fn with_description(mut self, description: bool) -> Self { @@ -50,50 +48,7 @@ where } /// Lints the project. - pub fn lint>( - self, - mut project: &Project, - ) -> eyre::Result> { - // TODO: infer linter from project - - // // Expand ignore globs and canonicalize paths - // let mut ignored = expand_globs(&root, config.fmt.ignore.iter())? - // .iter() - // .flat_map(foundry_common::fs::canonicalize_path) - // .collect::>(); - - // // Add explicitly excluded paths to the ignored set - // if let Some(exclude_paths) = &self.exclude { - // ignored.extend(exclude_paths.iter().flat_map(foundry_common::fs::canonicalize_path)); - // } - - // let mut input: Vec = if let Some(include_paths) = &self.include { - // include_paths.iter().filter(|path| path.exists()).cloned().collect() - // } else { - // source_files_iter(&root, SOLC_EXTENSIONS) - // .filter(|p| !(ignored.contains(p) || ignored.contains(&root.join(p)))) - // .collect() - // }; - - // input.retain(|path| !ignored.contains(path)); - - // if input.is_empty() { - // bail!("No source files found in path"); - // } - - if !project.paths.has_input_files() && self.files.is_empty() { - sh_println!("Nothing to lint")?; - std::process::exit(0); - } - - let sources = if !self.files.is_empty() { - Source::read_all(self.files.clone())? - } else { - project.paths.read_input_files()? - }; - - let input = sources.into_iter().map(|(path, _)| path).collect::>(); - + pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { Ok(self.linter.lint(&input).expect("TODO: handle error")) } } From 8c0be7336d6f6e87fbff6cdc2bf449cd255e9e44 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 02:11:03 -0500 Subject: [PATCH 045/107] linter output display note --- crates/forge/bin/cmd/lint.rs | 1 - crates/lint/src/lib.rs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 2455c5fde774a..905764e87df02 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -3,7 +3,6 @@ use eyre::Result; use forge_lint::sol::SolidityLinter; use forge_lint::{OutputFormat, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; -use foundry_compilers::artifacts::Source; use foundry_config::impl_figment_convert_basic; use std::collections::HashSet; use std::path::PathBuf; diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 1fd2b09d8b959..97f54e7cf6cbf 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -18,7 +18,7 @@ use std::{ }; use clap::ValueEnum; -use solar_ast::ast::{self, SourceUnit, Span}; +use solar_ast::ast::Span; pub struct ProjectLinter where @@ -138,6 +138,7 @@ impl SourceLocation { } } +// TODO: Update to implement Display for LinterOutput, model after compiler error display // impl fmt::Display for Error { // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // let mut short_msg = self.message.trim(); From 9fc264bba0397f75b39c59bfb4f5b3e1401702be Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 02:27:29 -0500 Subject: [PATCH 046/107] configure with severity and description --- crates/forge/bin/cmd/lint.rs | 2 ++ crates/lint/src/lib.rs | 38 +++++------------------------------- crates/lint/src/sol/mod.rs | 15 +++++++++++++- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 905764e87df02..32349c3dd9fe8 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -80,6 +80,8 @@ impl LintArgs { let linter = if project.compiler.solc.is_some() { SolidityLinter::new() + .with_severity(self.severity) + .with_description(self.with_description) } else { todo!("Linting not supported for this language"); }; diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 97f54e7cf6cbf..06130f7417a59 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,23 +1,15 @@ pub mod sol; -use foundry_common::sh_println; -use foundry_compilers::{ - artifacts::{Contract, Source}, - Compiler, CompilerContract, CompilerInput, Language, Project, -}; -use rayon::prelude::*; -use serde::{Deserialize, Serialize}; -use sol::SolidityLinter; use std::{ - collections::{BTreeMap, HashMap}, + collections::BTreeMap, error::Error, - hash::{Hash, Hasher}, - marker::PhantomData, + hash::Hash, ops::{Deref, DerefMut}, path::PathBuf, }; use clap::ValueEnum; +use foundry_compilers::Language; use solar_ast::ast::Span; pub struct ProjectLinter @@ -25,8 +17,6 @@ where L: Linter, { pub linter: L, - pub severity: Option>, - pub description: bool, } impl ProjectLinter @@ -34,20 +24,9 @@ where L: Linter, { pub fn new(linter: L) -> Self { - Self { linter, severity: None, description: false } - } - - pub fn with_description(mut self, description: bool) -> Self { - self.description = description; - self + Self { linter } } - pub fn with_severity(mut self, severity: Option>) -> Self { - self.severity = severity; - self - } - - /// Lints the project. pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { Ok(self.linter.lint(&input).expect("TODO: handle error")) } @@ -56,12 +35,10 @@ where // NOTE: add some way to specify linter profiles. For example having a profile adhering to the op stack, base, etc. // This can probably also be accomplished via the foundry.toml or some functions. Maybe have generic profile/settings -/// The main linter abstraction trait +// TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) pub trait Linter: Send + Sync + Clone { /// Enum of languages supported by the linter. type Language: Language; - // TODO: Add docs. This represents linter settings. (ex. Default, OP Stack, etc. - // type Settings: LinterSettings; type Lint: Lint + Ord; type LinterError: Error; @@ -69,11 +46,6 @@ pub trait Linter: Send + Sync + Clone { fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; } -// TODO: probably remove -pub trait LinterSettings { - fn lints() -> Vec; -} - pub struct LinterOutput(pub BTreeMap>); impl LinterOutput { diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index c77a54f36ec4c..79e9a53bd5f10 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -20,12 +20,25 @@ use thiserror::Error; use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; #[derive(Debug, Clone, Default)] -pub struct SolidityLinter; +pub struct SolidityLinter { + pub severity: Option>, + pub description: bool, +} impl SolidityLinter { pub fn new() -> Self { Self::default() } + + pub fn with_description(mut self, description: bool) -> Self { + self.description = description; + self + } + + pub fn with_severity(mut self, severity: Option>) -> Self { + self.severity = severity; + self + } } impl Linter for SolidityLinter { From f8e2c4d81cc7c2c6c2717c3dda632d28608b0f1e Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 29 Dec 2024 02:40:49 -0500 Subject: [PATCH 047/107] fmt --- crates/forge/bin/cmd/lint.rs | 6 ++---- crates/lint/src/lib.rs | 13 +++++++------ crates/lint/src/sol/mod.rs | 3 ++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 32349c3dd9fe8..38072050c4aa2 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,11 +1,9 @@ use clap::{Parser, ValueHint}; use eyre::Result; -use forge_lint::sol::SolidityLinter; -use forge_lint::{OutputFormat, ProjectLinter, Severity}; +use forge_lint::{sol::SolidityLinter, OutputFormat, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; use foundry_config::impl_figment_convert_basic; -use std::collections::HashSet; -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 06130f7417a59..dd6e2ad146f08 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -32,8 +32,9 @@ where } } -// NOTE: add some way to specify linter profiles. For example having a profile adhering to the op stack, base, etc. -// This can probably also be accomplished via the foundry.toml or some functions. Maybe have generic profile/settings +// NOTE: add some way to specify linter profiles. For example having a profile adhering to the op +// stack, base, etc. This can probably also be accomplished via the foundry.toml or some functions. +// Maybe have generic profile/settings // TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) pub trait Linter: Send + Sync + Clone { @@ -135,14 +136,14 @@ impl SourceLocation { // let mut lines = fmtd_msg.lines(); // // skip the first line if it contains the same message as the one we just formatted, -// // unless it also contains a source location, in which case the entire error message is an -// // old style error message, like: +// // unless it also contains a source location, in which case the entire error message is +// an // old style error message, like: // // path/to/file:line:column: ErrorType: message // if lines // .clone() // .next() -// .is_some_and(|l| l.contains(short_msg) && l.bytes().filter(|b| *b == b':').count() < 3) -// { +// .is_some_and(|l| l.contains(short_msg) && l.bytes().filter(|b| *b == b':').count() < +// 3) { // let _ = lines.next(); // } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 79e9a53bd5f10..bb06942e52af7 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -50,7 +50,8 @@ impl Linter for SolidityLinter { let all_findings = input .into_par_iter() .map(|file| { - // NOTE: use all solidity lints for now but this should be configurable via SolidityLinter + // NOTE: use all solidity lints for now but this should be configurable via + // SolidityLinter let mut lints = SolLint::all(); // Initialize session and parsing environment From 641b51a440cb49b6199b2447f6a9c2236884e1c9 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Tue, 31 Dec 2024 23:22:30 -0500 Subject: [PATCH 048/107] implementing display --- Cargo.lock | 1 + crates/forge/bin/cmd/lint.rs | 21 ++++------ crates/lint/Cargo.toml | 1 + crates/lint/src/lib.rs | 74 +++++++++++++++++++++++++++++------- crates/lint/src/sol/gas.rs | 18 ++++++--- crates/lint/src/sol/high.rs | 6 ++- crates/lint/src/sol/info.rs | 5 ++- crates/lint/src/sol/mod.rs | 4 ++ 8 files changed, 93 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78d3460fda9e6..2d938a0cb57c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3571,6 +3571,7 @@ dependencies = [ "solar-interface", "solar-parse", "thiserror 2.0.9", + "yansi", ] [[package]] diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 38072050c4aa2..415cd02ae7629 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -2,6 +2,7 @@ use clap::{Parser, ValueHint}; use eyre::Result; use forge_lint::{sol::SolidityLinter, OutputFormat, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; +use foundry_common::shell; use foundry_config::impl_figment_convert_basic; use std::{collections::HashSet, path::PathBuf}; @@ -47,6 +48,8 @@ impl_figment_convert_basic!(LintArgs); impl LintArgs { pub fn run(self) -> Result<()> { + let now = std::time::Instant::now(); + let config = self.try_load_config_emit_warnings()?; let project = config.project()?; @@ -56,13 +59,9 @@ impl LintArgs { // Add included paths to sources if let Some(include_paths) = &self.include { - let included = include_paths - .iter() - .filter(|path| sources.contains(path)) - .cloned() - .collect::>(); - - sources = included; + let included = + include_paths.iter().filter(|path| path.exists()).cloned().collect::>(); + sources.extend(included); } // Remove excluded files from sources @@ -85,13 +84,9 @@ impl LintArgs { }; let output = ProjectLinter::new(linter).lint(&sources)?; + sh_println!("{}", &output)?; - // TODO: display output - // let format_json = shell::is_json(); - - // if format_json && !self.names && !self.sizes { - // sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?; - // } + dbg!(now.elapsed()); Ok(()) } diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 2e9fcb22601d4..93458cebabab2 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -27,6 +27,7 @@ rayon.workspace = true thiserror.workspace = true serde_json.workspace = true auto_impl.workspace = true +yansi.workspace = true serde = { workspace = true, features = ["derive"] } regex = "1.11" clap = { version = "4", features = ["derive"] } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index dd6e2ad146f08..391d2dedd2993 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,8 +1,10 @@ pub mod sol; +use core::fmt; use std::{ collections::BTreeMap, error::Error, + fmt::Display, hash::Hash, ops::{Deref, DerefMut}, path::PathBuf, @@ -10,7 +12,20 @@ use std::{ use clap::ValueEnum; use foundry_compilers::Language; +use serde::Serialize; use solar_ast::ast::Span; +use yansi::Paint; + +// TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) +pub trait Linter: Send + Sync + Clone { + /// Enum of languages supported by the linter. + type Language: Language; + type Lint: Lint + Ord; + type LinterError: Error; + + /// Main entrypoint for the linter. + fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; +} pub struct ProjectLinter where @@ -36,17 +51,6 @@ where // stack, base, etc. This can probably also be accomplished via the foundry.toml or some functions. // Maybe have generic profile/settings -// TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) -pub trait Linter: Send + Sync + Clone { - /// Enum of languages supported by the linter. - type Language: Language; - type Lint: Lint + Ord; - type LinterError: Error; - - /// Main entrypoint for the linter. - fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; -} - pub struct LinterOutput(pub BTreeMap>); impl LinterOutput { @@ -78,6 +82,36 @@ impl Extend<(L::Lint, Vec)> for LinterOutput { } } +impl fmt::Display for LinterOutput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (lint, locations) in &self.0 { + // Get lint details + let severity = lint.severity(); + let name = lint.name(); + let description = lint.description(); + + // Write the main message + writeln!(f, "{severity}: {name}: {description}")?; + + // Write the source locations + for location in locations { + // writeln!( + // f, + // " --> {}:{}:{}", + // location.file.display(), + // location.line(), + // location.column() + // )?; + // writeln!(f, " |")?; + // writeln!(f, "{} | {}", location.line(), "^".repeat(location.column() as usize))?; + // writeln!(f, " |")?; + } + } + + Ok(()) + } +} + pub trait Lint: Hash { fn name(&self) -> &'static str; fn description(&self) -> &'static str; @@ -99,6 +133,20 @@ pub enum Severity { Info, Gas, } + +impl fmt::Display for Severity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let colored = match self { + Severity::High => Paint::red("High").bold(), + Severity::Med => Paint::yellow("Med").bold(), + Severity::Low => Paint::green("Low").bold(), + Severity::Info => Paint::blue("Info").bold(), + Severity::Gas => Paint::green("Gas").bold(), + }; + write!(f, "{}", colored) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SourceLocation { pub file: PathBuf, @@ -143,9 +191,7 @@ impl SourceLocation { // .clone() // .next() // .is_some_and(|l| l.contains(short_msg) && l.bytes().filter(|b| *b == b':').count() < -// 3) { -// let _ = lines.next(); -// } +// 3) { let _ = lines.next(); } // // format the main source location // fmt_source_location(f, &mut lines)?; diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 5bae74a4baf59..5eaf4851b5de8 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -23,37 +23,43 @@ impl<'ast> Visit<'ast> for AsmKeccak256 { impl<'ast> Visit<'ast> for PackStorageVariables { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } impl<'ast> Visit<'ast> for PackStructs { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } impl<'ast> Visit<'ast> for UseConstantVariable { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } impl<'ast> Visit<'ast> for UseImmutableVariable { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } impl<'ast> Visit<'ast> for UseExternalVisibility { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } impl<'ast> Visit<'ast> for AvoidUsingThis { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high.rs index 49e08bf56cc75..ff2477af2a8fa 100644 --- a/crates/lint/src/sol/high.rs +++ b/crates/lint/src/sol/high.rs @@ -4,12 +4,14 @@ use super::{ArbitraryTransferFrom, IncorrectShift}; impl<'ast> Visit<'ast> for IncorrectShift { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + // TODO: + self.walk_expr(expr); } } impl<'ast> Visit<'ast> for ArbitraryTransferFrom { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - todo!() + //TODO: + self.walk_expr(expr); } } diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 9c0bf47699ee2..490eeeb31c7d9 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -48,8 +48,9 @@ impl<'ast> Visit<'ast> for StructPascalCase { } impl Visit<'_> for FunctionCamelCase { - fn visit_function_header(&mut self, header: &'_ solar_ast::ast::FunctionHeader<'_>) { - todo!() + fn visit_function_header(&mut self, header: &solar_ast::ast::FunctionHeader<'_>) { + // TODO: + // self.walk_function_header(header); } } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index bb06942e52af7..f23cd151dbc18 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -85,6 +85,10 @@ impl Linter for SolidityLinter { .map(|span| SourceLocation::new(file.clone(), *span)) .collect::>(); + if source_locations.is_empty() { + continue; + } + output.insert(lint, source_locations); } } From cbd6b2ba39365c7625e77b51ff96f93767d18d79 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 01:21:18 -0500 Subject: [PATCH 049/107] wip --- crates/forge/bin/cmd/lint.rs | 13 +- crates/lint/src/lib.rs | 259 ++++++++++++++++++++++++----------- 2 files changed, 179 insertions(+), 93 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 415cd02ae7629..20b256060d991 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,8 +1,7 @@ use clap::{Parser, ValueHint}; use eyre::Result; -use forge_lint::{sol::SolidityLinter, OutputFormat, ProjectLinter, Severity}; +use forge_lint::{sol::SolidityLinter, ProjectLinter, Severity}; use foundry_cli::utils::LoadConfig; -use foundry_common::shell; use foundry_config::impl_figment_convert_basic; use std::{collections::HashSet, path::PathBuf}; @@ -24,13 +23,6 @@ pub struct LintArgs { #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] exclude: Option>, - // TODO: support writing to output file - /// Format of the output. - /// - /// Supported values: `json` or `markdown`. - #[arg(long, value_name = "FORMAT", default_value = "json")] - format: OutputFormat, - /// Specifies which lints to run based on severity. /// /// Supported values: `high`, `med`, `low`, `info`, `gas`. @@ -84,9 +76,8 @@ impl LintArgs { }; let output = ProjectLinter::new(linter).lint(&sources)?; - sh_println!("{}", &output)?; - dbg!(now.elapsed()); + sh_println!("{}", &output)?; Ok(()) } diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 391d2dedd2993..34af6ba7241f2 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -5,6 +5,7 @@ use std::{ collections::BTreeMap, error::Error, fmt::Display, + fs, hash::Hash, ops::{Deref, DerefMut}, path::PathBuf, @@ -13,8 +14,10 @@ use std::{ use clap::ValueEnum; use foundry_compilers::Language; use serde::Serialize; +use sol::high; use solar_ast::ast::Span; -use yansi::Paint; +use solar_interface::BytePos; +use yansi::{Paint, Painted}; // TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) pub trait Linter: Send + Sync + Clone { @@ -84,27 +87,132 @@ impl Extend<(L::Lint, Vec)> for LinterOutput { impl fmt::Display for LinterOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Add initial spacing before output + writeln!(f, "")?; + for (lint, locations) in &self.0 { - // Get lint details let severity = lint.severity(); let name = lint.name(); let description = lint.description(); - // Write the main message - writeln!(f, "{severity}: {name}: {description}")?; - - // Write the source locations for location in locations { - // writeln!( - // f, - // " --> {}:{}:{}", - // location.file.display(), - // location.line(), - // location.column() - // )?; - // writeln!(f, " |")?; - // writeln!(f, "{} | {}", location.line(), "^".repeat(location.column() as usize))?; - // writeln!(f, " |")?; + let file_content = std::fs::read_to_string(&location.file) + .expect("Could not read file for source location"); + let lo_offset = location.span.lo().0 as usize; + let hi_offset = location.span.hi().0 as usize; + + if lo_offset > file_content.len() || hi_offset > file_content.len() { + continue; // Skip if offsets are out of bounds + } + + let mut offset = 0; + let mut start_line = None; + let mut start_column = None; + let mut end_line = None; + let mut end_column = None; + + for (line_number, line) in file_content.lines().enumerate() { + let line_length = line.len() + 1; + + // Calculate start position + if start_line.is_none() && + offset <= lo_offset && + lo_offset < offset + line_length + { + start_line = Some(line_number + 1); + start_column = Some(lo_offset - offset + 1); + } + + // Calculate end position + if end_line.is_none() && offset <= hi_offset && hi_offset < offset + line_length + { + end_line = Some(line_number + 1); + end_column = Some(hi_offset - offset + 1); + break; + } + + offset += line_length; + } + + let (start_line, start_column) = ( + start_line.expect("Start line not found"), + start_column.expect("Start column not found"), + ); + let (end_line, end_column) = ( + end_line.expect("End line not found"), + end_column.expect("End column not found"), + ); + + let max_line_number_width = start_line.to_string().len(); + + writeln!(f, "{severity}: {name}: {description}")?; + + writeln!( + f, + "{} {}:{}:{}", + Paint::blue(" -->").bold(), + location.file.display(), + start_line, + start_column + )?; + + writeln!( + f, + "{:width$}{}", + "", + Paint::blue("|").bold(), + width = max_line_number_width + 1 + )?; + + let lines = file_content.lines().collect::>(); + let display_start_line = if start_line > 1 { start_line - 1 } else { start_line }; + let display_end_line = if end_line < lines.len() { end_line + 1 } else { end_line }; + + for line_number in display_start_line..=display_end_line { + let line = lines.get(line_number - 1).unwrap_or(&""); + + if line_number == start_line { + writeln!( + f, + "{:>width$} {} {}", + line_number, + Paint::blue("|").bold(), + line, + width = max_line_number_width + )?; + + let caret = + severity.color(&"^".repeat((end_column - start_column + 1) as usize)); + writeln!( + f, + "{:width$}{} {}{}", + "", + Paint::blue("|").bold(), + " ".repeat((start_column - 1) as usize), + caret, + width = max_line_number_width + 1 + )?; + } else { + writeln!( + f, + "{:width$}{} {}", + "", + Paint::blue("|").bold(), + line, + width = max_line_number_width + 1 + )?; + } + } + + writeln!( + f, + "{:width$}{}", + "", + Paint::blue("|").bold(), + width = max_line_number_width + 1 + )?; + + writeln!(f, "")?; } } @@ -118,12 +226,6 @@ pub trait Lint: Hash { fn severity(&self) -> Severity; } -#[derive(Clone, Debug, ValueEnum)] -pub enum OutputFormat { - Json, - Markdown, -} - // TODO: impl color for severity #[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum Severity { @@ -134,14 +236,26 @@ pub enum Severity { Gas, } +impl Severity { + pub fn color(&self, message: &str) -> String { + match self { + Severity::High => Paint::red(message).bold().to_string(), + Severity::Med => Paint::yellow(message).bold().to_string(), + Severity::Low => Paint::green(message).bold().to_string(), + Severity::Info => Paint::blue(message).bold().to_string(), + Severity::Gas => Paint::green(message).bold().to_string(), + } + } +} + impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let colored = match self { - Severity::High => Paint::red("High").bold(), - Severity::Med => Paint::yellow("Med").bold(), - Severity::Low => Paint::green("Low").bold(), - Severity::Info => Paint::blue("Info").bold(), - Severity::Gas => Paint::green("Gas").bold(), + Severity::High => self.color("High"), + Severity::Med => self.color("Med"), + Severity::Low => self.color("Low"), + Severity::Info => self.color("Info"), + Severity::Gas => self.color("Gas"), }; write!(f, "{}", colored) } @@ -157,59 +271,40 @@ impl SourceLocation { pub fn new(file: PathBuf, span: Span) -> Self { Self { file, span } } -} -// TODO: Update to implement Display for LinterOutput, model after compiler error display -// impl fmt::Display for Error { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// let mut short_msg = self.message.trim(); -// let fmtd_msg = self.formatted_message.as_deref().unwrap_or(""); - -// if short_msg.is_empty() { -// // if the message is empty, try to extract the first line from the formatted message -// if let Some(first_line) = fmtd_msg.lines().next() { -// // this is something like `ParserError: ` -// if let Some((_, s)) = first_line.split_once(':') { -// short_msg = s.trim_start(); -// } else { -// short_msg = first_line; -// } -// } -// } - -// // Error (XXXX): Error Message -// styled(f, self.severity.color().bold(), |f| self.fmt_severity(f))?; -// fmt_msg(f, short_msg)?; - -// let mut lines = fmtd_msg.lines(); - -// // skip the first line if it contains the same message as the one we just formatted, -// // unless it also contains a source location, in which case the entire error message is -// an // old style error message, like: -// // path/to/file:line:column: ErrorType: message -// if lines -// .clone() -// .next() -// .is_some_and(|l| l.contains(short_msg) && l.bytes().filter(|b| *b == b':').count() < -// 3) { let _ = lines.next(); } - -// // format the main source location -// fmt_source_location(f, &mut lines)?; - -// // format remaining lines as secondary locations -// while let Some(line) = lines.next() { -// f.write_str("\n")?; - -// if let Some((note, msg)) = line.split_once(':') { -// styled(f, Self::secondary_style(), |f| f.write_str(note))?; -// fmt_msg(f, msg)?; -// } else { -// f.write_str(line)?; -// } - -// fmt_source_location(f, &mut lines)?; -// } - -// Ok(()) -// } -// } + /// Compute the line and column for the start and end of the span. + pub fn location(&self) -> Option<((usize, usize), (usize, usize))> { + let file_content = fs::read_to_string(&self.file).ok()?; + let lo = self.span.lo().0 as usize; + let hi = self.span.hi().0 as usize; + + if lo > file_content.len() || hi > file_content.len() { + return None; + } + + let mut offset = 0; + let mut start_line = None; + let mut start_column = None; + + for (line_number, line) in file_content.lines().enumerate() { + let line_length = line.len() + 1; + + // If start line and column is already found, look for end line and column + if let Some(start) = start_line { + if offset <= hi && hi < offset + line_length { + let end_line = line_number + 1; + let end_column = hi - offset + 1; + return Some(((start, start_column.unwrap()), (end_line, end_column))); + } + } else if offset <= lo && lo < offset + line_length { + // Determine start line and column. + start_line = Some(line_number + 1); + start_column = Some(lo - offset + 1); + } + + offset += line_length; + } + + None + } +} From 18dadde00353c74f8067cf45e8acba4b2f61469e Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 01:45:21 -0500 Subject: [PATCH 050/107] wip --- crates/lint/src/lib.rs | 165 ++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 100 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 34af6ba7241f2..c5e427f11fef2 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -96,123 +96,85 @@ impl fmt::Display for LinterOutput { let description = lint.description(); for location in locations { - let file_content = std::fs::read_to_string(&location.file) - .expect("Could not read file for source location"); - let lo_offset = location.span.lo().0 as usize; - let hi_offset = location.span.hi().0 as usize; - - if lo_offset > file_content.len() || hi_offset > file_content.len() { - continue; // Skip if offsets are out of bounds - } - - let mut offset = 0; - let mut start_line = None; - let mut start_column = None; - let mut end_line = None; - let mut end_column = None; - - for (line_number, line) in file_content.lines().enumerate() { - let line_length = line.len() + 1; - - // Calculate start position - if start_line.is_none() && - offset <= lo_offset && - lo_offset < offset + line_length + if let Some(file_contents) = location.file_contents() { + if let Some(((start_line, start_column), (end_line, end_column))) = + location.location(&file_contents) { - start_line = Some(line_number + 1); - start_column = Some(lo_offset - offset + 1); - } - - // Calculate end position - if end_line.is_none() && offset <= hi_offset && hi_offset < offset + line_length - { - end_line = Some(line_number + 1); - end_column = Some(hi_offset - offset + 1); - break; - } + dbg!(start_line, start_column, end_line, end_column); + let max_line_number_width = end_line.to_string().len(); - offset += line_length; - } + writeln!(f, "{severity}: {name}: {description}")?; - let (start_line, start_column) = ( - start_line.expect("Start line not found"), - start_column.expect("Start column not found"), - ); - let (end_line, end_column) = ( - end_line.expect("End line not found"), - end_column.expect("End column not found"), - ); - - let max_line_number_width = start_line.to_string().len(); - - writeln!(f, "{severity}: {name}: {description}")?; - - writeln!( - f, - "{} {}:{}:{}", - Paint::blue(" -->").bold(), - location.file.display(), - start_line, - start_column - )?; - - writeln!( - f, - "{:width$}{}", - "", - Paint::blue("|").bold(), - width = max_line_number_width + 1 - )?; - - let lines = file_content.lines().collect::>(); - let display_start_line = if start_line > 1 { start_line - 1 } else { start_line }; - let display_end_line = if end_line < lines.len() { end_line + 1 } else { end_line }; - - for line_number in display_start_line..=display_end_line { - let line = lines.get(line_number - 1).unwrap_or(&""); - - if line_number == start_line { writeln!( f, - "{:>width$} {} {}", - line_number, - Paint::blue("|").bold(), - line, - width = max_line_number_width + "{} {}:{}:{}", + Paint::blue(" -->").bold(), + location.file.display(), + start_line, + start_column )?; - let caret = - severity.color(&"^".repeat((end_column - start_column + 1) as usize)); writeln!( f, - "{:width$}{} {}{}", + "{:width$}{}", "", Paint::blue("|").bold(), - " ".repeat((start_column - 1) as usize), - caret, width = max_line_number_width + 1 )?; - } else { + + let lines = file_contents.lines().collect::>(); + let display_start_line = + if start_line > 1 { start_line - 1 } else { start_line }; + let display_end_line = + if end_line < lines.len() { end_line + 1 } else { end_line }; + + for line_number in display_start_line..=display_end_line { + let line = lines.get(line_number - 1).unwrap_or(&""); + + if line_number == start_line { + writeln!( + f, + "{:>width$} {} {}", + line_number, + Paint::blue("|").bold(), + line, + width = max_line_number_width + )?; + + let caret = severity + .color(&"^".repeat((end_column - start_column + 1) as usize)); + writeln!( + f, + "{:width$}{} {}{}", + "", + Paint::blue("|").bold(), + " ".repeat((start_column - 1) as usize), + caret, + width = max_line_number_width + 1 + )?; + } else { + writeln!( + f, + "{:width$}{} {}", + "", + Paint::blue("|").bold(), + line, + width = max_line_number_width + 1 + )?; + } + } + writeln!( f, - "{:width$}{} {}", + "{:width$}{}", "", Paint::blue("|").bold(), - line, width = max_line_number_width + 1 )?; + + writeln!(f, "")?; } } - - writeln!( - f, - "{:width$}{}", - "", - Paint::blue("|").bold(), - width = max_line_number_width + 1 - )?; - - writeln!(f, "")?; } } @@ -272,13 +234,16 @@ impl SourceLocation { Self { file, span } } + pub fn file_contents(&self) -> Option { + fs::read_to_string(&self.file).ok() + } + /// Compute the line and column for the start and end of the span. - pub fn location(&self) -> Option<((usize, usize), (usize, usize))> { - let file_content = fs::read_to_string(&self.file).ok()?; + pub fn location(&self, file_contents: &str) -> Option<((usize, usize), (usize, usize))> { let lo = self.span.lo().0 as usize; let hi = self.span.hi().0 as usize; - if lo > file_content.len() || hi > file_content.len() { + if lo > file_contents.len() || hi > file_contents.len() { return None; } @@ -286,7 +251,7 @@ impl SourceLocation { let mut start_line = None; let mut start_column = None; - for (line_number, line) in file_content.lines().enumerate() { + for (line_number, line) in file_contents.lines().enumerate() { let line_length = line.len() + 1; // If start line and column is already found, look for end line and column From b86a7cff5d9ecdd96556780a12005599abbb1f19 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 02:10:41 -0500 Subject: [PATCH 051/107] implement display for linter output, clippy fixes --- crates/forge/bin/cmd/lint.rs | 3 - crates/lint/src/lib.rs | 202 ++++++++++++++++------------------- crates/lint/src/sol/info.rs | 2 +- 3 files changed, 95 insertions(+), 112 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 20b256060d991..062f931c941ff 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -40,8 +40,6 @@ impl_figment_convert_basic!(LintArgs); impl LintArgs { pub fn run(self) -> Result<()> { - let now = std::time::Instant::now(); - let config = self.try_load_config_emit_warnings()?; let project = config.project()?; @@ -76,7 +74,6 @@ impl LintArgs { }; let output = ProjectLinter::new(linter).lint(&sources)?; - sh_println!("{}", &output)?; Ok(()) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index c5e427f11fef2..d04f644569172 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,23 +1,17 @@ pub mod sol; +use clap::ValueEnum; use core::fmt; +use foundry_compilers::Language; +use solar_ast::ast::Span; use std::{ collections::BTreeMap, error::Error, - fmt::Display, - fs, hash::Hash, ops::{Deref, DerefMut}, path::PathBuf, }; - -use clap::ValueEnum; -use foundry_compilers::Language; -use serde::Serialize; -use sol::high; -use solar_ast::ast::Span; -use solar_interface::BytePos; -use yansi::{Paint, Painted}; +use yansi::Paint; // TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) pub trait Linter: Send + Sync + Clone { @@ -46,7 +40,7 @@ where } pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { - Ok(self.linter.lint(&input).expect("TODO: handle error")) + Ok(self.linter.lint(input).expect("TODO: handle error")) } } @@ -54,12 +48,13 @@ where // stack, base, etc. This can probably also be accomplished via the foundry.toml or some functions. // Maybe have generic profile/settings +#[derive(Default)] pub struct LinterOutput(pub BTreeMap>); impl LinterOutput { // Optional: You can still provide a `new` method for convenience pub fn new() -> Self { - LinterOutput(BTreeMap::new()) + Self(BTreeMap::new()) } } @@ -80,15 +75,14 @@ impl DerefMut for LinterOutput { impl Extend<(L::Lint, Vec)> for LinterOutput { fn extend)>>(&mut self, iter: T) { for (lint, findings) in iter { - self.0.entry(lint).or_insert_with(Vec::new).extend(findings); + self.0.entry(lint).or_default().extend(findings); } } } impl fmt::Display for LinterOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Add initial spacing before output - writeln!(f, "")?; + writeln!(f)?; for (lint, locations) in &self.0 { let severity = lint.severity(); @@ -96,85 +90,82 @@ impl fmt::Display for LinterOutput { let description = lint.description(); for location in locations { - if let Some(file_contents) = location.file_contents() { - if let Some(((start_line, start_column), (end_line, end_column))) = - location.location(&file_contents) - { - dbg!(start_line, start_column, end_line, end_column); - let max_line_number_width = end_line.to_string().len(); - - writeln!(f, "{severity}: {name}: {description}")?; - + let file_content = std::fs::read_to_string(&location.file) + .expect("Could not read file for source location"); + + let ((start_line, start_column), (end_line, end_column)) = + match location.location(&file_content) { + Some(pos) => pos, + None => continue, + }; + + let max_line_number_width = start_line.to_string().len(); + + writeln!(f, "{severity}: {name}: {description}")?; + writeln!( + f, + "{} {}:{}:{}", + Paint::blue(" -->").bold(), + location.file.display(), + start_line, + start_column + )?; + writeln!( + f, + "{:width$}{}", + "", + Paint::blue("|").bold(), + width = max_line_number_width + 1 + )?; + + let lines: Vec<&str> = file_content.lines().collect(); + let display_start_line = if start_line > 1 { start_line - 1 } else { start_line }; + let display_end_line = if end_line < lines.len() { end_line + 1 } else { end_line }; + + for line_number in display_start_line..=display_end_line { + let line = lines.get(line_number - 1).unwrap_or(&""); + + if line_number == start_line { writeln!( f, - "{} {}:{}:{}", - Paint::blue(" -->").bold(), - location.file.display(), - start_line, - start_column + "{:>width$} {} {}", + line_number, + Paint::blue("|").bold(), + line, + width = max_line_number_width )?; + let caret = severity.color(&"^".repeat(end_column - start_column + 1)); writeln!( f, - "{:width$}{}", + "{:width$}{} {}{}", "", Paint::blue("|").bold(), + " ".repeat(start_column - 1), + caret, width = max_line_number_width + 1 )?; - - let lines = file_contents.lines().collect::>(); - let display_start_line = - if start_line > 1 { start_line - 1 } else { start_line }; - let display_end_line = - if end_line < lines.len() { end_line + 1 } else { end_line }; - - for line_number in display_start_line..=display_end_line { - let line = lines.get(line_number - 1).unwrap_or(&""); - - if line_number == start_line { - writeln!( - f, - "{:>width$} {} {}", - line_number, - Paint::blue("|").bold(), - line, - width = max_line_number_width - )?; - - let caret = severity - .color(&"^".repeat((end_column - start_column + 1) as usize)); - writeln!( - f, - "{:width$}{} {}{}", - "", - Paint::blue("|").bold(), - " ".repeat((start_column - 1) as usize), - caret, - width = max_line_number_width + 1 - )?; - } else { - writeln!( - f, - "{:width$}{} {}", - "", - Paint::blue("|").bold(), - line, - width = max_line_number_width + 1 - )?; - } - } - + } else { writeln!( f, - "{:width$}{}", + "{:width$}{} {}", "", Paint::blue("|").bold(), + line, width = max_line_number_width + 1 )?; - - writeln!(f, "")?; } } + + writeln!( + f, + "{:width$}{}", + "", + Paint::blue("|").bold(), + width = max_line_number_width + 1 + )?; + + writeln!(f)?; } } @@ -201,11 +192,11 @@ pub enum Severity { impl Severity { pub fn color(&self, message: &str) -> String { match self { - Severity::High => Paint::red(message).bold().to_string(), - Severity::Med => Paint::yellow(message).bold().to_string(), - Severity::Low => Paint::green(message).bold().to_string(), - Severity::Info => Paint::blue(message).bold().to_string(), - Severity::Gas => Paint::green(message).bold().to_string(), + Self::High => Paint::red(message).bold().to_string(), + Self::Med => Paint::yellow(message).bold().to_string(), + Self::Low => Paint::green(message).bold().to_string(), + Self::Info => Paint::blue(message).bold().to_string(), + Self::Gas => Paint::green(message).bold().to_string(), } } } @@ -213,13 +204,13 @@ impl Severity { impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let colored = match self { - Severity::High => self.color("High"), - Severity::Med => self.color("Med"), - Severity::Low => self.color("Low"), - Severity::Info => self.color("Info"), - Severity::Gas => self.color("Gas"), + Self::High => self.color("High"), + Self::Med => self.color("Med"), + Self::Low => self.color("Low"), + Self::Info => self.color("Info"), + Self::Gas => self.color("Gas"), }; - write!(f, "{}", colored) + write!(f, "{colored}") } } @@ -233,38 +224,33 @@ impl SourceLocation { pub fn new(file: PathBuf, span: Span) -> Self { Self { file, span } } - - pub fn file_contents(&self) -> Option { - fs::read_to_string(&self.file).ok() - } - /// Compute the line and column for the start and end of the span. - pub fn location(&self, file_contents: &str) -> Option<((usize, usize), (usize, usize))> { + pub fn location(&self, file_content: &str) -> Option<((usize, usize), (usize, usize))> { let lo = self.span.lo().0 as usize; let hi = self.span.hi().0 as usize; - if lo > file_contents.len() || hi > file_contents.len() { + // Ensure offsets are valid + if lo > file_content.len() || hi > file_content.len() || lo > hi { return None; } let mut offset = 0; - let mut start_line = None; - let mut start_column = None; + let mut start_line = 0; + let mut start_column = 0; - for (line_number, line) in file_contents.lines().enumerate() { + for (line_number, line) in file_content.lines().enumerate() { let line_length = line.len() + 1; - // If start line and column is already found, look for end line and column - if let Some(start) = start_line { - if offset <= hi && hi < offset + line_length { - let end_line = line_number + 1; - let end_column = hi - offset + 1; - return Some(((start, start_column.unwrap()), (end_line, end_column))); - } - } else if offset <= lo && lo < offset + line_length { - // Determine start line and column. - start_line = Some(line_number + 1); - start_column = Some(lo - offset + 1); + // Check start position + if offset <= lo && lo < offset + line_length { + start_line = line_number + 1; + start_column = lo - offset + 1; + } + + // Check end position + if offset <= hi && hi < offset + line_length { + // Return if both positions are found + return Some(((start_line, start_column), (line_number + 1, hi - offset + 1))); } offset += line_length; diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 490eeeb31c7d9..e3e50f19d2134 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -48,7 +48,7 @@ impl<'ast> Visit<'ast> for StructPascalCase { } impl Visit<'_> for FunctionCamelCase { - fn visit_function_header(&mut self, header: &solar_ast::ast::FunctionHeader<'_>) { + fn visit_function_header(&mut self, _header: &solar_ast::ast::FunctionHeader<'_>) { // TODO: // self.walk_function_header(header); } From dda0366ff29bfee4aac9fbd33fb5e4d4ecd2e8ca Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 02:15:58 -0500 Subject: [PATCH 052/107] add note to update colors --- crates/lint/src/lib.rs | 7 ++++--- crates/lint/testdata/Keccak256.sol | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index d04f644569172..c30bb2da2993c 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -190,12 +190,13 @@ pub enum Severity { } impl Severity { + // TODO: update colors pub fn color(&self, message: &str) -> String { match self { Self::High => Paint::red(message).bold().to_string(), - Self::Med => Paint::yellow(message).bold().to_string(), - Self::Low => Paint::green(message).bold().to_string(), - Self::Info => Paint::blue(message).bold().to_string(), + Self::Med => Paint::magenta(message).bold().to_string(), + Self::Low => Paint::yellow(message).bold().to_string(), + Self::Info => Paint::cyan(message).bold().to_string(), Self::Gas => Paint::green(message).bold().to_string(), } } diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol index b908c5727d621..4ec2be94d6878 100644 --- a/crates/lint/testdata/Keccak256.sol +++ b/crates/lint/testdata/Keccak256.sol @@ -4,7 +4,6 @@ contract Contract0 { } function solidityHash(uint256 a, uint256 b) public view { - //unoptimized keccak256(abi.encodePacked(a, b)); } From 384110dc033532aaf5841fa8fa7f3a4cb83b3552 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 02:31:55 -0500 Subject: [PATCH 053/107] update linter output display --- crates/lint/src/lib.rs | 8 +++++++- crates/lint/src/sol/mod.rs | 11 ++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index c30bb2da2993c..e1990c9145269 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -101,7 +101,13 @@ impl fmt::Display for LinterOutput { let max_line_number_width = start_line.to_string().len(); - writeln!(f, "{severity}: {name}: {description}")?; + writeln!( + f, + "{}: {}: {}", + severity, + Paint::white(name).bold(), + Paint::white(description).bold() + )?; writeln!( f, "{} {}:{}:{}", diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index f23cd151dbc18..993975a1d0d6c 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -219,15 +219,20 @@ declare_sol_lints!( (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), // Med - (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "TODO: description"), + ( + DivideBeforeMultiply, + Severity::Med, + "divide-before-multiply", + "Multiplication should occur before division to avoid loss of precision." + ), // Low // Info - (VariableCamelCase, Severity::Info, "variable-camel-case", "TODO: description"), + (VariableCamelCase, Severity::Info, "variable-camel-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables."), (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "TODO: description"), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas."), (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), From c7dd9b41374075d6e153cc9178bd5f76f3018163 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 15:14:24 -0500 Subject: [PATCH 054/107] remove todos, clean up comments --- crates/lint/src/lib.rs | 9 --------- crates/lint/src/sol/mod.rs | 4 +--- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index e1990c9145269..2b65ef258d97f 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -13,14 +13,11 @@ use std::{ }; use yansi::Paint; -// TODO: maybe add a way to specify the linter "profile" (ex. Default, OP Stack, etc.) pub trait Linter: Send + Sync + Clone { - /// Enum of languages supported by the linter. type Language: Language; type Lint: Lint + Ord; type LinterError: Error; - /// Main entrypoint for the linter. fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; } @@ -44,15 +41,10 @@ where } } -// NOTE: add some way to specify linter profiles. For example having a profile adhering to the op -// stack, base, etc. This can probably also be accomplished via the foundry.toml or some functions. -// Maybe have generic profile/settings - #[derive(Default)] pub struct LinterOutput(pub BTreeMap>); impl LinterOutput { - // Optional: You can still provide a `new` method for convenience pub fn new() -> Self { Self(BTreeMap::new()) } @@ -185,7 +177,6 @@ pub trait Lint: Hash { fn severity(&self) -> Severity; } -// TODO: impl color for severity #[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum Severity { High, diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 993975a1d0d6c..3296f976e8a24 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -50,8 +50,7 @@ impl Linter for SolidityLinter { let all_findings = input .into_par_iter() .map(|file| { - // NOTE: use all solidity lints for now but this should be configurable via - // SolidityLinter + // TODO: this should be configurable let mut lints = SolLint::all(); // Initialize session and parsing environment @@ -103,7 +102,6 @@ pub enum SolLintError {} macro_rules! declare_sol_lints { ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { - // TODO: ord based on severity #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SolLint { $( From 45b7b3a828e321dbb49b9064b4fd4a1acb9dba41 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 15:31:24 -0500 Subject: [PATCH 055/107] clean up display --- crates/lint/src/lib.rs | 57 +++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 2b65ef258d97f..71e4a2f91d89d 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -5,7 +5,7 @@ use core::fmt; use foundry_compilers::Language; use solar_ast::ast::Span; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, error::Error, hash::Hash, ops::{Deref, DerefMut}, @@ -81,9 +81,14 @@ impl fmt::Display for LinterOutput { let name = lint.name(); let description = lint.description(); + let mut file_contents = HashMap::new(); + for location in locations { - let file_content = std::fs::read_to_string(&location.file) - .expect("Could not read file for source location"); + let file_content = + file_contents.entry(location.file.clone()).or_insert_with(|| { + std::fs::read_to_string(&location.file) + .expect("Could not read file for source location") + }); let ((start_line, start_column), (end_line, end_column)) = match location.location(&file_content) { @@ -116,23 +121,24 @@ impl fmt::Display for LinterOutput { width = max_line_number_width + 1 )?; - let lines: Vec<&str> = file_content.lines().collect(); - let display_start_line = if start_line > 1 { start_line - 1 } else { start_line }; - let display_end_line = if end_line < lines.len() { end_line + 1 } else { end_line }; - - for line_number in display_start_line..=display_end_line { + let lines = file_content.lines().collect::>(); + for line_number in start_line..=end_line { let line = lines.get(line_number - 1).unwrap_or(&""); - if line_number == start_line { - writeln!( - f, - "{:>width$} {} {}", - line_number, - Paint::blue("|").bold(), - line, - width = max_line_number_width - )?; + writeln!( + f, + "{:>width$} {} {}", + if line_number == start_line { + line_number.to_string() + } else { + String::new() + }, + Paint::blue("|").bold(), + line, + width = max_line_number_width + )?; + if line_number == start_line { let caret = severity.color(&"^".repeat(end_column - start_column + 1)); writeln!( f, @@ -143,26 +149,9 @@ impl fmt::Display for LinterOutput { caret, width = max_line_number_width + 1 )?; - } else { - writeln!( - f, - "{:width$}{} {}", - "", - Paint::blue("|").bold(), - line, - width = max_line_number_width + 1 - )?; } } - writeln!( - f, - "{:width$}{}", - "", - Paint::blue("|").bold(), - width = max_line_number_width + 1 - )?; - writeln!(f)?; } } From 1a01afa70e0bd3ec49c63d41ede986dad49b5ca3 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 15:45:29 -0500 Subject: [PATCH 056/107] update med finding color --- crates/lint/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 71e4a2f91d89d..bbc582b58d01a 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -180,7 +180,7 @@ impl Severity { pub fn color(&self, message: &str) -> String { match self { Self::High => Paint::red(message).bold().to_string(), - Self::Med => Paint::magenta(message).bold().to_string(), + Self::Med => Paint::rgb(message, 255, 135, 61).bold().to_string(), Self::Low => Paint::yellow(message).bold().to_string(), Self::Info => Paint::cyan(message).bold().to_string(), Self::Gas => Paint::green(message).bold().to_string(), From a4d7bd3c538bd0f3492779c80f72dd46212f777a Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 16:43:29 -0500 Subject: [PATCH 057/107] add optional help message --- crates/lint/src/lib.rs | 4 ++-- crates/lint/src/sol/mod.rs | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index bbc582b58d01a..29abb4e3cdb1b 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -16,7 +16,7 @@ use yansi::Paint; pub trait Linter: Send + Sync + Clone { type Language: Language; type Lint: Lint + Ord; - type LinterError: Error; + type LinterError: Error + Send + Sync + 'static; fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; } @@ -37,7 +37,7 @@ where } pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { - Ok(self.linter.lint(input).expect("TODO: handle error")) + Ok(self.linter.lint(input)?) } } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 3296f976e8a24..e23e7e12df3f8 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -100,8 +100,7 @@ impl Linter for SolidityLinter { pub enum SolLintError {} macro_rules! declare_sol_lints { - ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr)),* $(,)?) => { - + ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr $(, $help:expr)?)),* $(,)?) => { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SolLint { $( @@ -126,7 +125,6 @@ macro_rules! declare_sol_lints { } } - /// Lint a source unit and return the findings pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { match self { $( @@ -137,6 +135,14 @@ macro_rules! declare_sol_lints { )* } } + + pub fn help(&self) -> Option<&'static str> { + match self { + $( + SolLint::$name(_) => $name::help(), + )* + } + } } impl<'ast> Visit<'ast> for SolLint { @@ -181,7 +187,6 @@ macro_rules! declare_sol_lints { } } - $( #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct $name { @@ -193,20 +198,22 @@ macro_rules! declare_sol_lints { Self { results: Vec::new() } } - /// Returns the severity of the lint pub fn severity() -> Severity { $severity } - /// Returns the name of the lint pub fn name() -> &'static str { $lint_name } - /// Returns the description of the lint pub fn description() -> &'static str { $description } + + pub const fn help() -> Option<&'static str> { + $(Some($help))? + None + } } )* }; @@ -230,7 +237,7 @@ declare_sol_lints!( (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas."), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "for further information visit https://xyz.com"), (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), From 04c6fccb86e30b81a61885211261b7e09bcc022d Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 17:29:38 -0500 Subject: [PATCH 058/107] display help message --- crates/lint/src/lib.rs | 13 +++++++++++++ crates/lint/src/sol/mod.rs | 29 +++++------------------------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 29abb4e3cdb1b..a4650c92b227f 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -152,6 +152,18 @@ impl fmt::Display for LinterOutput { } } + if let Some(help) = lint.help() { + writeln!( + f, + "{:width$}{} {}{}", + "", + Paint::blue("=").bold(), + Paint::white("help:").bold(), + help, + width = max_line_number_width + 1 + )?; + } + writeln!(f)?; } } @@ -163,6 +175,7 @@ impl fmt::Display for LinterOutput { pub trait Lint: Hash { fn name(&self) -> &'static str; fn description(&self) -> &'static str; + fn help(&self) -> Option<&'static str>; fn severity(&self) -> Severity; } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index e23e7e12df3f8..51f0fe7435cd0 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -135,14 +135,6 @@ macro_rules! declare_sol_lints { )* } } - - pub fn help(&self) -> Option<&'static str> { - match self { - $( - SolLint::$name(_) => $name::help(), - )* - } - } } impl<'ast> Visit<'ast> for SolLint { @@ -185,6 +177,11 @@ macro_rules! declare_sol_lints { )* } } + + // TODO: + fn help(&self) -> Option<&'static str> { + None + } } $( @@ -198,22 +195,6 @@ macro_rules! declare_sol_lints { Self { results: Vec::new() } } - pub fn severity() -> Severity { - $severity - } - - pub fn name() -> &'static str { - $lint_name - } - - pub fn description() -> &'static str { - $description - } - - pub const fn help() -> Option<&'static str> { - $(Some($help))? - None - } } )* }; From ff882861bf6ed747bafa357716dc8a478e48d90a Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 18:17:22 -0500 Subject: [PATCH 059/107] simplify lint args, make severity configurable --- crates/forge/bin/cmd/lint.rs | 21 ++--------- crates/lint/src/lib.rs | 9 ++--- crates/lint/src/sol/mod.rs | 70 ++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 53 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 062f931c941ff..938e95dac8abf 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -8,18 +8,11 @@ use std::{collections::HashSet, path::PathBuf}; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] pub struct LintArgs { - /// 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, - - /// Include only the specified files when linting. + /// Include additional files to lint. #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] include: Option>, - /// Exclude the specified files when linting. + /// Exclude specified files when linting. #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] exclude: Option>, @@ -28,12 +21,6 @@ pub struct LintArgs { /// Supported values: `high`, `med`, `low`, `info`, `gas`. #[arg(long, value_name = "SEVERITY", num_args(1..))] severity: Option>, - - /// Show descriptions in the output. - /// - /// Disabled by default to avoid long console output. - #[arg(long)] - with_description: bool, } impl_figment_convert_basic!(LintArgs); @@ -66,9 +53,7 @@ impl LintArgs { } let linter = if project.compiler.solc.is_some() { - SolidityLinter::new() - .with_severity(self.severity) - .with_description(self.with_description) + SolidityLinter::new().with_severity(self.severity) } else { todo!("Linting not supported for this language"); }; diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index a4650c92b227f..d5008ff488686 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -152,14 +152,15 @@ impl fmt::Display for LinterOutput { } } - if let Some(help) = lint.help() { + if let Some(url) = lint.url() { writeln!( f, - "{:width$}{} {}{}", + "{:width$}{} {} {} {}", "", Paint::blue("=").bold(), Paint::white("help:").bold(), - help, + Paint::white("for further information visit"), + url, width = max_line_number_width + 1 )?; } @@ -175,7 +176,7 @@ impl fmt::Display for LinterOutput { pub trait Lint: Hash { fn name(&self) -> &'static str; fn description(&self) -> &'static str; - fn help(&self) -> Option<&'static str>; + fn url(&self) -> Option<&'static str>; fn severity(&self) -> Severity; } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 51f0fe7435cd0..8e3aac51da7b9 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -50,8 +50,11 @@ impl Linter for SolidityLinter { let all_findings = input .into_par_iter() .map(|file| { - // TODO: this should be configurable - let mut lints = SolLint::all(); + let mut lints = if let Some(severity) = &self.severity { + SolLint::with_severity(severity.to_owned()) + } else { + SolLint::all() + }; // Initialize session and parsing environment let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); @@ -100,7 +103,7 @@ impl Linter for SolidityLinter { pub enum SolLintError {} macro_rules! declare_sol_lints { - ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr $(, $help:expr)?)),* $(,)?) => { + ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr, $url:expr)),* $(,)?) => { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SolLint { $( @@ -125,6 +128,13 @@ macro_rules! declare_sol_lints { } } + pub fn with_severity(severity: Vec) -> Vec { + Self::all() + .into_iter() + .filter(|lint| severity.contains(&lint.severity())) + .collect() + } + pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { match self { $( @@ -178,9 +188,18 @@ macro_rules! declare_sol_lints { } } - // TODO: - fn help(&self) -> Option<&'static str> { - None + fn url(&self) -> Option<&'static str> { + match self { + $( + SolLint::$name(_) => { + if !$url.is_empty() { + Some($url) + } else { + None + } + }, + )* + } } } @@ -194,7 +213,6 @@ macro_rules! declare_sol_lints { pub fn new() -> Self { Self { results: Vec::new() } } - } )* }; @@ -202,32 +220,22 @@ macro_rules! declare_sol_lints { declare_sol_lints!( //High - (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description"), - (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description"), + (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description", ""), + (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description", ""), // Med - ( - DivideBeforeMultiply, - Severity::Med, - "divide-before-multiply", - "Multiplication should occur before division to avoid loss of precision." - ), + (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "Multiplication should occur before division to avoid loss of precision.", ""), // Low // Info - (VariableCamelCase, Severity::Info, "variable-camel-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables."), - (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description"), - (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description"), - (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description"), + (VariableCamelCase, Severity::Info, "variable-camel-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables.", ""), + (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description", ""), + (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description", ""), + (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description", ""), // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "for further information visit https://xyz.com"), - (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description"), - (PackStructs, Severity::Gas, "pack-structs", "TODO: description"), - (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description"), - (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description"), - (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description"), - ( - AvoidUsingThis, - Severity::Gas, - "avoid-using-this", - "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL." - ), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "https://placeholder.xyz"), + (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description", ""), + (PackStructs, Severity::Gas, "pack-structs", "TODO: description", ""), + (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description", ""), + (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description", ""), + (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description", ""), + (AvoidUsingThis, Severity::Gas, "avoid-using-this", "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL.", ""), ); From 50292ee8504ebc22ad6ca9df6a2933b01d839ba6 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 18:41:46 -0500 Subject: [PATCH 060/107] updating lints, update tests --- crates/forge/bin/cmd/lint.rs | 11 +- crates/lint/src/lib.rs | 1 - crates/lint/src/sol/gas.rs | 49 +------- crates/lint/src/sol/high.rs | 9 +- crates/lint/src/sol/info.rs | 127 ++++++++++++++++++++- crates/lint/src/sol/mod.rs | 22 ++-- crates/lint/testdata/VariableMixedCase.sol | 19 +++ 7 files changed, 161 insertions(+), 77 deletions(-) create mode 100644 crates/lint/testdata/VariableMixedCase.sol diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index 938e95dac8abf..a13c9fe1b9618 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -8,11 +8,18 @@ use std::{collections::HashSet, path::PathBuf}; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] pub struct LintArgs { - /// Include additional files to lint. + /// 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, + + /// Include only the specified files when linting. #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] include: Option>, - /// Exclude specified files when linting. + /// Exclude the specified files when linting. #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] exclude: Option>, diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index d5008ff488686..ca8e5033e0b4e 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -190,7 +190,6 @@ pub enum Severity { } impl Severity { - // TODO: update colors pub fn color(&self, message: &str) -> String { match self { Self::High => Paint::red(message).bold().to_string(), diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 5eaf4851b5de8..490ec00100852 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -3,10 +3,7 @@ use solar_ast::{ visit::Visit, }; -use super::{ - AsmKeccak256, AvoidUsingThis, PackStorageVariables, PackStructs, UseConstantVariable, - UseExternalVisibility, UseImmutableVariable, -}; +use super::AsmKeccak256; impl<'ast> Visit<'ast> for AsmKeccak256 { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { @@ -21,50 +18,6 @@ impl<'ast> Visit<'ast> for AsmKeccak256 { } } -impl<'ast> Visit<'ast> for PackStorageVariables { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // TODO: - self.walk_expr(expr); - } -} - -impl<'ast> Visit<'ast> for PackStructs { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // TODO: - self.walk_expr(expr); - } -} - -impl<'ast> Visit<'ast> for UseConstantVariable { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // TODO: - self.walk_expr(expr); - } -} - -impl<'ast> Visit<'ast> for UseImmutableVariable { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // TODO: - self.walk_expr(expr); - } -} - -impl<'ast> Visit<'ast> for UseExternalVisibility { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // TODO: - self.walk_expr(expr); - } -} - -impl<'ast> Visit<'ast> for AvoidUsingThis { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - // TODO: - self.walk_expr(expr); - } -} - -// TODO: avoid using `this` to read public variables - #[cfg(test)] mod test { use solar_ast::{ast, visit::Visit}; diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high.rs index ff2477af2a8fa..8ae10a4f5d1be 100644 --- a/crates/lint/src/sol/high.rs +++ b/crates/lint/src/sol/high.rs @@ -1,6 +1,6 @@ use solar_ast::{ast::Expr, visit::Visit}; -use super::{ArbitraryTransferFrom, IncorrectShift}; +use super::IncorrectShift; impl<'ast> Visit<'ast> for IncorrectShift { fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { @@ -8,10 +8,3 @@ impl<'ast> Visit<'ast> for IncorrectShift { self.walk_expr(expr); } } - -impl<'ast> Visit<'ast> for ArbitraryTransferFrom { - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) { - //TODO: - self.walk_expr(expr); - } -} diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index e3e50f19d2134..47e705e33bb75 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -5,14 +5,14 @@ use solar_ast::{ visit::Visit, }; -use super::{FunctionCamelCase, StructPascalCase, VariableCamelCase, VariableCapsCase}; +use super::{FunctionMixedCase, StructPascalCase, VariableCapsCase, VariableMixedCase}; -impl<'ast> Visit<'ast> for VariableCamelCase { +impl<'ast> Visit<'ast> for VariableMixedCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { if let Some(mutability) = var.mutability { if !mutability.is_constant() && !mutability.is_immutable() { if let Some(name) = var.name { - if !is_camel_case(name.as_str()) { + if !is_mixed_case(name.as_str()) { self.results.push(var.span); } } @@ -47,7 +47,7 @@ impl<'ast> Visit<'ast> for StructPascalCase { } } -impl Visit<'_> for FunctionCamelCase { +impl Visit<'_> for FunctionMixedCase { fn visit_function_header(&mut self, _header: &solar_ast::ast::FunctionHeader<'_>) { // TODO: // self.walk_function_header(header); @@ -55,7 +55,7 @@ impl Visit<'_> for FunctionCamelCase { } // Check if a string is camelCase -pub fn is_camel_case(s: &str) -> bool { +pub fn is_mixed_case(s: &str) -> bool { let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); re.is_match(s) && s.chars().any(|c| c.is_uppercase()) } @@ -66,8 +66,123 @@ pub fn is_pascal_case(s: &str) -> bool { re.is_match(s) } -// Check if a string is SCREAMING_SNAKE_CASE +// Check if a string is CAPS_CASE pub fn is_caps_case(s: &str) -> bool { let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); re.is_match(s) && s.contains('_') } + +#[cfg(test)] +mod test { + use solar_ast::{ast, visit::Visit}; + use solar_interface::{ColorChoice, Session}; + use std::path::Path; + + use crate::sol::{FunctionMixedCase, StructPascalCase}; + + use super::{VariableCapsCase, VariableMixedCase}; + + #[test] + fn test_variable_mixed_case() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = solar_parse::Parser::from_file( + &sess, + &arena, + Path::new("testdata/VariableMixedCase.sol"), + )?; + + let ast = parser.parse_file().map_err(|e| e.emit())?; + + let mut pattern = VariableMixedCase::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.results.len(), 3); + + Ok(()) + }); + + Ok(()) + } + + #[test] + fn test_variable_caps_case() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = solar_parse::Parser::from_file( + &sess, + &arena, + Path::new("testdata/VariableCapsCase.sol"), + )?; + + let ast = parser.parse_file().map_err(|e| e.emit())?; + + let mut pattern = VariableCapsCase::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.results.len(), 3); + + Ok(()) + }); + + Ok(()) + } + + #[test] + fn test_struct_pascal_case() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = solar_parse::Parser::from_file( + &sess, + &arena, + Path::new("testdata/StructPascalCase.sol"), + )?; + + let ast = parser.parse_file().map_err(|e| e.emit())?; + + let mut pattern = StructPascalCase::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.results.len(), 3); + + Ok(()) + }); + + Ok(()) + } + + #[test] + fn test_function_mixed_case() -> eyre::Result<()> { + let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + + let _ = sess.enter(|| -> solar_interface::Result<()> { + let arena = ast::Arena::new(); + + let mut parser = solar_parse::Parser::from_file( + &sess, + &arena, + Path::new("testdata/FunctionMixedCase.sol"), + )?; + + let ast = parser.parse_file().map_err(|e| e.emit())?; + + let mut pattern = FunctionMixedCase::default(); + pattern.visit_source_unit(&ast); + + assert_eq!(pattern.results.len(), 3); + + Ok(()) + }); + + Ok(()) + } +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 8e3aac51da7b9..329dd59e9fb37 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -220,22 +220,20 @@ macro_rules! declare_sol_lints { declare_sol_lints!( //High - (IncorrectShift, Severity::High, "incorrect-shift", "TODO: description", ""), - (ArbitraryTransferFrom, Severity::High, "arbitrary-transfer-from", "TODO: description", ""), + (IncorrectShift, Severity::High, "incorrect-shift", "The order of args in a shift operation is incorrect.", ""), // Med (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "Multiplication should occur before division to avoid loss of precision.", ""), // Low // Info - (VariableCamelCase, Severity::Info, "variable-camel-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables.", ""), - (VariableCapsCase, Severity::Info, "variable-caps-case", "TODO: description", ""), - (StructPascalCase, Severity::Info, "struct-pascal-case", "TODO: description", ""), - (FunctionCamelCase, Severity::Info, "function-camel-case", "TODO: description", ""), + (VariableMixedCase, Severity::Info, "variable-mixed-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables.", ""), + (VariableCapsCase, Severity::Info, "variable-caps-case", "Constants should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names"), + (StructPascalCase, Severity::Info, "struct-pascal-case", "Structs should be named using CapWords style. Examples: MyCoin, Position", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names"), + (FunctionMixedCase, Severity::Info, "function-mixed-case", "Constants should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#function-names"), // Gas Optimizations (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "https://placeholder.xyz"), - (PackStorageVariables, Severity::Gas, "pack-storage-variables", "TODO: description", ""), - (PackStructs, Severity::Gas, "pack-structs", "TODO: description", ""), - (UseConstantVariable, Severity::Gas, "use-constant-var", "TODO: description", ""), - (UseImmutableVariable, Severity::Gas, "use-immutable-var", "TODO: description", ""), - (UseExternalVisibility, Severity::Gas, "use-external-visibility", "TODO: description", ""), - (AvoidUsingThis, Severity::Gas, "avoid-using-this", "Avoid using `this` to read public variables. This incurs an unncessary STATICCALL.", ""), + // TODO: PackStorageVariables + // TODO: PackStructs + // TODO: UseConstantVariable + // TODO: UseImmutableVariable + // TODO: UseCalldataInsteadOfMemory ); diff --git a/crates/lint/testdata/VariableMixedCase.sol b/crates/lint/testdata/VariableMixedCase.sol new file mode 100644 index 0000000000000..50c6827d8fa4a --- /dev/null +++ b/crates/lint/testdata/VariableMixedCase.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract VariableMixedCase { + uint256 variableMixedCase; + uint256 _variableMixedCase; + uint256 Variablemixedcase; + uint256 VARIABLE_MIXED_CASE; + uint256 variablemixedcase; + + function foo() public { + uint256 testVal = 1; + uint256 testVAL = 2; + uint256 TestVal = 3; + uint256 TESTVAL = 4; + uint256 tESTVAL = 5; + uint256 test6Val = 6; + } +} From 721ec17e9b55145dfb69f5e69158cf1af25a5e98 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 19:39:04 -0500 Subject: [PATCH 061/107] add tests for info patterns, fix regex --- crates/lint/src/lib.rs | 2 +- crates/lint/src/sol/info.rs | 41 +++++++++---------- crates/lint/src/sol/mod.rs | 4 +- crates/lint/testdata/DivideBeforeMultiply.sol | 2 +- crates/lint/testdata/Keccak256.sol | 2 +- crates/lint/testdata/ScreamingSnakeCase.sol | 22 ++++++++++ crates/lint/testdata/StructPascalCase.sol | 38 +++++++++++++++++ crates/lint/testdata/VariableMixedCase.sol | 20 +++++---- 8 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 crates/lint/testdata/ScreamingSnakeCase.sol create mode 100644 crates/lint/testdata/StructPascalCase.sol diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index ca8e5033e0b4e..f0d0b209b21c9 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -139,7 +139,7 @@ impl fmt::Display for LinterOutput { )?; if line_number == start_line { - let caret = severity.color(&"^".repeat(end_column - start_column + 1)); + let caret = severity.color(&"^".repeat(end_column - start_column)); writeln!( f, "{:width$}{} {}{}", diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 47e705e33bb75..f9001b5ce427c 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -5,29 +5,28 @@ use solar_ast::{ visit::Visit, }; -use super::{FunctionMixedCase, StructPascalCase, VariableCapsCase, VariableMixedCase}; +use super::{FunctionMixedCase, ScreamingSnakeCase, StructPascalCase, VariableMixedCase}; impl<'ast> Visit<'ast> for VariableMixedCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { - if let Some(mutability) = var.mutability { - if !mutability.is_constant() && !mutability.is_immutable() { - if let Some(name) = var.name { - if !is_mixed_case(name.as_str()) { - self.results.push(var.span); - } + if var.mutability.is_none() { + if let Some(name) = var.name { + if !is_mixed_case(name.as_str()) { + self.results.push(var.span); } } } + self.walk_variable_definition(var); } } -impl<'ast> Visit<'ast> for VariableCapsCase { +impl<'ast> Visit<'ast> for ScreamingSnakeCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { if let Some(mutability) = var.mutability { if mutability.is_constant() || mutability.is_immutable() { if let Some(name) = var.name { - if !is_caps_case(name.as_str()) { + if !is_screaming_snake_case(name.as_str()) { self.results.push(var.span); } } @@ -54,7 +53,7 @@ impl Visit<'_> for FunctionMixedCase { } } -// Check if a string is camelCase +// Check if a string is mixedCase pub fn is_mixed_case(s: &str) -> bool { let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); re.is_match(s) && s.chars().any(|c| c.is_uppercase()) @@ -62,13 +61,13 @@ pub fn is_mixed_case(s: &str) -> bool { // Check if a string is PascalCase pub fn is_pascal_case(s: &str) -> bool { - let re = Regex::new(r"^[A-Z0-9][a-zA-Z0-9]*$").unwrap(); + let re = Regex::new(r"^[A-Z][a-z]+(?:[A-Z][a-z]+)*$").unwrap(); re.is_match(s) } -// Check if a string is CAPS_CASE -pub fn is_caps_case(s: &str) -> bool { - let re = Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap(); +// Check if a string is SCREAMING_SNAKE_CASE +pub fn is_screaming_snake_case(s: &str) -> bool { + let re = Regex::new(r"^[A-Z_][A-Z0-9_]*$").unwrap(); re.is_match(s) && s.contains('_') } @@ -80,7 +79,7 @@ mod test { use crate::sol::{FunctionMixedCase, StructPascalCase}; - use super::{VariableCapsCase, VariableMixedCase}; + use super::{ScreamingSnakeCase, VariableMixedCase}; #[test] fn test_variable_mixed_case() -> eyre::Result<()> { @@ -100,7 +99,7 @@ mod test { let mut pattern = VariableMixedCase::default(); pattern.visit_source_unit(&ast); - assert_eq!(pattern.results.len(), 3); + assert_eq!(pattern.results.len(), 6); Ok(()) }); @@ -109,7 +108,7 @@ mod test { } #[test] - fn test_variable_caps_case() -> eyre::Result<()> { + fn test_screaming_snake_case() -> eyre::Result<()> { let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); let _ = sess.enter(|| -> solar_interface::Result<()> { @@ -118,15 +117,15 @@ mod test { let mut parser = solar_parse::Parser::from_file( &sess, &arena, - Path::new("testdata/VariableCapsCase.sol"), + Path::new("testdata/ScreamingSnakeCase.sol"), )?; let ast = parser.parse_file().map_err(|e| e.emit())?; - let mut pattern = VariableCapsCase::default(); + let mut pattern = ScreamingSnakeCase::default(); pattern.visit_source_unit(&ast); - assert_eq!(pattern.results.len(), 3); + assert_eq!(pattern.results.len(), 10); Ok(()) }); @@ -152,7 +151,7 @@ mod test { let mut pattern = StructPascalCase::default(); pattern.visit_source_unit(&ast); - assert_eq!(pattern.results.len(), 3); + assert_eq!(pattern.results.len(), 5); Ok(()) }); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 329dd59e9fb37..9c504755f6a11 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -226,8 +226,8 @@ declare_sol_lints!( // Low // Info (VariableMixedCase, Severity::Info, "variable-mixed-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables.", ""), - (VariableCapsCase, Severity::Info, "variable-caps-case", "Constants should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names"), - (StructPascalCase, Severity::Info, "struct-pascal-case", "Structs should be named using CapWords style. Examples: MyCoin, Position", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names"), + (ScreamingSnakeCase, Severity::Info, "screaming-snake-case", "Constants and immutables should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names"), + (StructPascalCase, Severity::Info, "struct-pascal-case", "Structs should be named using PascalCase. Examples: MyCoin, Position", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names"), (FunctionMixedCase, Severity::Info, "function-mixed-case", "Constants should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#function-names"), // Gas Optimizations (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "https://placeholder.xyz"), diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol index 8e3e9525f7ba1..3bde4b9cf6c9f 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.sol +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -1,4 +1,4 @@ -contract Contract0 { +contract DivideBeforeMultiply { function arithmetic() public { (1 / 2) * 3; // Unsafe (1 * 2) / 3; // Safe diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol index 4ec2be94d6878..12fcbc4c00c21 100644 --- a/crates/lint/testdata/Keccak256.sol +++ b/crates/lint/testdata/Keccak256.sol @@ -1,4 +1,4 @@ -contract Contract0 { +contract AsmKeccak256 { constructor(uint256 a, uint256 b) { keccak256(abi.encodePacked(a, b)); } diff --git a/crates/lint/testdata/ScreamingSnakeCase.sol b/crates/lint/testdata/ScreamingSnakeCase.sol new file mode 100644 index 0000000000000..ca4d72fb6d06d --- /dev/null +++ b/crates/lint/testdata/ScreamingSnakeCase.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ScreamingSnakeCaseTest { + // Passes + uint256 constant _SCREAMING_SNAKE_CASE = 0; + uint256 constant SCREAMING_SNAKE_CASE = 0; + uint256 immutable _SCREAMING_SNAKE_CASE_1 = 0; + uint256 immutable SCREAMING_SNAKE_CASE_1 = 0; + + // Fails + uint256 constant SCREAMINGSNAKECASE = 0; + uint256 constant screamingSnakeCase = 0; + uint256 constant screaming_snake_case = 0; + uint256 constant ScreamingSnakeCase = 0; + uint256 constant SCREAMING_snake_case = 0; + uint256 immutable SCREAMINGSNAKECASE0 = 0; + uint256 immutable screamingSnakeCase0 = 0; + uint256 immutable screaming_snake_case0 = 0; + uint256 immutable ScreamingSnakeCase0 = 0; + uint256 immutable SCREAMING_snake_case_0 = 0; +} diff --git a/crates/lint/testdata/StructPascalCase.sol b/crates/lint/testdata/StructPascalCase.sol new file mode 100644 index 0000000000000..a79758f24baf9 --- /dev/null +++ b/crates/lint/testdata/StructPascalCase.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract StructPascalCaseTest { + // Passes + struct PascalCase { + uint256 a; + } + + // Fails + struct _PascalCase { + uint256 a; + } + + struct pascalCase { + uint256 a; + } + + struct pascalcase { + uint256 a; + } + + struct pascal_case { + uint256 a; + } + + struct PASCAL_CASE { + uint256 a; + } + + struct PASCALCASE { + uint256 a; + } + + struct PascalCAse { + uint256 a; + } +} diff --git a/crates/lint/testdata/VariableMixedCase.sol b/crates/lint/testdata/VariableMixedCase.sol index 50c6827d8fa4a..77f28b226196b 100644 --- a/crates/lint/testdata/VariableMixedCase.sol +++ b/crates/lint/testdata/VariableMixedCase.sol @@ -1,19 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -contract VariableMixedCase { +contract VariableMixedCaseTest { + // Passes uint256 variableMixedCase; uint256 _variableMixedCase; + + // Fails uint256 Variablemixedcase; uint256 VARIABLE_MIXED_CASE; uint256 variablemixedcase; + uint256 VariableMixedCase; function foo() public { - uint256 testVal = 1; - uint256 testVAL = 2; - uint256 TestVal = 3; - uint256 TESTVAL = 4; - uint256 tESTVAL = 5; - uint256 test6Val = 6; + // Passes + uint256 testVal; + uint256 testVAL; + uint256 testVal123; + + // Fails + uint256 TestVal; + uint256 TESTVAL; } } From 1e2d403da6abb3182d0e696d6d80a2721b385cf9 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 19:42:11 -0500 Subject: [PATCH 062/107] remove function mixed case --- crates/lint/src/sol/info.rs | 37 ++----------------------------------- crates/lint/src/sol/mod.rs | 6 +++++- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index f9001b5ce427c..2b118e3e8327a 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -5,7 +5,7 @@ use solar_ast::{ visit::Visit, }; -use super::{FunctionMixedCase, ScreamingSnakeCase, StructPascalCase, VariableMixedCase}; +use super::{ScreamingSnakeCase, StructPascalCase, VariableMixedCase}; impl<'ast> Visit<'ast> for VariableMixedCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { @@ -46,13 +46,6 @@ impl<'ast> Visit<'ast> for StructPascalCase { } } -impl Visit<'_> for FunctionMixedCase { - fn visit_function_header(&mut self, _header: &solar_ast::ast::FunctionHeader<'_>) { - // TODO: - // self.walk_function_header(header); - } -} - // Check if a string is mixedCase pub fn is_mixed_case(s: &str) -> bool { let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); @@ -77,7 +70,7 @@ mod test { use solar_interface::{ColorChoice, Session}; use std::path::Path; - use crate::sol::{FunctionMixedCase, StructPascalCase}; + use crate::sol::StructPascalCase; use super::{ScreamingSnakeCase, VariableMixedCase}; @@ -158,30 +151,4 @@ mod test { Ok(()) } - - #[test] - fn test_function_mixed_case() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = ast::Arena::new(); - - let mut parser = solar_parse::Parser::from_file( - &sess, - &arena, - Path::new("testdata/FunctionMixedCase.sol"), - )?; - - let ast = parser.parse_file().map_err(|e| e.emit())?; - - let mut pattern = FunctionMixedCase::default(); - pattern.visit_source_unit(&ast); - - assert_eq!(pattern.results.len(), 3); - - Ok(()) - }); - - Ok(()) - } } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 9c504755f6a11..f89acebfc22bb 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -220,15 +220,19 @@ macro_rules! declare_sol_lints { declare_sol_lints!( //High + (IncorrectShift, Severity::High, "incorrect-shift", "The order of args in a shift operation is incorrect.", ""), // Med + (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "Multiplication should occur before division to avoid loss of precision.", ""), // Low + // Info (VariableMixedCase, Severity::Info, "variable-mixed-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables.", ""), (ScreamingSnakeCase, Severity::Info, "screaming-snake-case", "Constants and immutables should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names"), (StructPascalCase, Severity::Info, "struct-pascal-case", "Structs should be named using PascalCase. Examples: MyCoin, Position", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names"), - (FunctionMixedCase, Severity::Info, "function-mixed-case", "Constants should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#function-names"), + // TODO: FunctionMixedCase + // Gas Optimizations (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "https://placeholder.xyz"), // TODO: PackStorageVariables From ce13d5fd46bdb87c5c6f845f77ccc6f8b857ceda Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 20:06:19 -0500 Subject: [PATCH 063/107] doc comments --- crates/lint/src/lib.rs | 18 +++++++++++++++++- crates/lint/src/sol/mod.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index f0d0b209b21c9..76a2a64af61e1 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -13,6 +13,20 @@ use std::{ }; use yansi::Paint; +/// Trait representing a generic linter for analyzing and reporting issues in smart contract source +/// code files. A linter can be implemented for any smart contract lanugage supported by Foundry. +/// +/// # Type Parameters +/// +/// - `Language`: Represents the target programming language. Must implement the [`Language`] trait. +/// - `Lint`: Represents the types of lints performed by the linter. Must implement the [`Lint`] +/// trait. +/// - `LinterError`: Represents errors that can occur during the linting process. +/// +/// # Required Methods +/// +/// - `lint`: Scans the provided source files and returns a [`LinterOutput`] containing categorized +/// findings or an error if linting fails. pub trait Linter: Send + Sync + Clone { type Language: Language; type Lint: Lint + Ord; @@ -214,6 +228,7 @@ impl fmt::Display for Severity { } } +/// Represents the location of a specific AST node in the specified `file`. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SourceLocation { pub file: PathBuf, @@ -224,7 +239,8 @@ impl SourceLocation { pub fn new(file: PathBuf, span: Span) -> Self { Self { file, span } } - /// Compute the line and column for the start and end of the span. + + /// Find the start and end position of the span in the file content. pub fn location(&self, file_content: &str) -> Option<((usize, usize), (usize, usize))> { let lo = self.span.lo().0 as usize; let hi = self.span.hi().0 as usize; diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index f89acebfc22bb..2fd0a66ca2b71 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -19,6 +19,9 @@ use thiserror::Error; use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; + +/// A linter implementation to analyze Solidity source code responsible for identifying vulnerabilities +/// gas optimizations, and best practices. #[derive(Debug, Clone, Default)] pub struct SolidityLinter { pub severity: Option>, @@ -102,6 +105,20 @@ impl Linter for SolidityLinter { #[derive(Error, Debug)] pub enum SolLintError {} + +/// Macro for defining lints and relevant metadata for the Solidity linter. +/// +/// This macro generates the [`SolLint`] enum with each lint along with utility methods and +/// corresponding structs for each lint specified. +/// +/// # Parameters +/// +/// Each lint is defined as a tuple with the following fields: +/// - `$name`: Identitifier used for the struct and enum variant created for the lint. +/// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). +/// - `$lint_name`: A string identifier for the lint used during reporting. +/// - `$description`: A short description of the lint. +/// - `$url`: URL providing additional information about the lint or best practices. macro_rules! declare_sol_lints { ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr, $url:expr)),* $(,)?) => { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] From beb716fe95301d589348a2578fd72102eabafdb5 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 20:07:01 -0500 Subject: [PATCH 064/107] clippy --- crates/lint/src/lib.rs | 2 +- crates/lint/src/sol/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 76a2a64af61e1..c45d0f18eb718 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -105,7 +105,7 @@ impl fmt::Display for LinterOutput { }); let ((start_line, start_column), (end_line, end_column)) = - match location.location(&file_content) { + match location.location(file_content) { Some(pos) => pos, None => continue, }; diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 2fd0a66ca2b71..1db86997e8c67 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -20,7 +20,7 @@ use thiserror::Error; use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; -/// A linter implementation to analyze Solidity source code responsible for identifying vulnerabilities +/// Linter implementation to analyze Solidity source code responsible for identifying vulnerabilities /// gas optimizations, and best practices. #[derive(Debug, Clone, Default)] pub struct SolidityLinter { From c06da6e44f0fc20c47e460dba98362a28f442b1e Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 20:07:26 -0500 Subject: [PATCH 065/107] fmt --- crates/lint/src/sol/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 1db86997e8c67..6f7057929877d 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -19,9 +19,8 @@ use thiserror::Error; use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; - -/// Linter implementation to analyze Solidity source code responsible for identifying vulnerabilities -/// gas optimizations, and best practices. +/// Linter implementation to analyze Solidity source code responsible for identifying +/// vulnerabilities gas optimizations, and best practices. #[derive(Debug, Clone, Default)] pub struct SolidityLinter { pub severity: Option>, @@ -105,10 +104,9 @@ impl Linter for SolidityLinter { #[derive(Error, Debug)] pub enum SolLintError {} - /// Macro for defining lints and relevant metadata for the Solidity linter. /// -/// This macro generates the [`SolLint`] enum with each lint along with utility methods and +/// This macro generates the [`SolLint`] enum with each lint along with utility methods and /// corresponding structs for each lint specified. /// /// # Parameters @@ -237,7 +235,6 @@ macro_rules! declare_sol_lints { declare_sol_lints!( //High - (IncorrectShift, Severity::High, "incorrect-shift", "The order of args in a shift operation is incorrect.", ""), // Med From 775af51f85f7bcb46dd89f0d1eeb7225edb5e29c Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 20:51:04 -0500 Subject: [PATCH 066/107] reorganize, crate level docs --- crates/forge/bin/cmd/lint.rs | 5 +- crates/lint/src/lib.rs | 280 +---------------------------------- crates/lint/src/linter.rs | 275 ++++++++++++++++++++++++++++++++++ crates/lint/src/sol/mod.rs | 2 +- 4 files changed, 284 insertions(+), 278 deletions(-) create mode 100644 crates/lint/src/linter.rs diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index a13c9fe1b9618..d84827dc878a3 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,6 +1,9 @@ use clap::{Parser, ValueHint}; use eyre::Result; -use forge_lint::{sol::SolidityLinter, ProjectLinter, Severity}; +use forge_lint::{ + linter::{ProjectLinter, Severity}, + sol::SolidityLinter, +}; use foundry_cli::utils::LoadConfig; use foundry_config::impl_figment_convert_basic; use std::{collections::HashSet, path::PathBuf}; diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index c45d0f18eb718..73e1b3c95427c 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,277 +1,5 @@ +//! # forge-lint +//! +//! Types, traits, and utilities for linting Solidity projects. +pub mod linter; pub mod sol; - -use clap::ValueEnum; -use core::fmt; -use foundry_compilers::Language; -use solar_ast::ast::Span; -use std::{ - collections::{BTreeMap, HashMap}, - error::Error, - hash::Hash, - ops::{Deref, DerefMut}, - path::PathBuf, -}; -use yansi::Paint; - -/// Trait representing a generic linter for analyzing and reporting issues in smart contract source -/// code files. A linter can be implemented for any smart contract lanugage supported by Foundry. -/// -/// # Type Parameters -/// -/// - `Language`: Represents the target programming language. Must implement the [`Language`] trait. -/// - `Lint`: Represents the types of lints performed by the linter. Must implement the [`Lint`] -/// trait. -/// - `LinterError`: Represents errors that can occur during the linting process. -/// -/// # Required Methods -/// -/// - `lint`: Scans the provided source files and returns a [`LinterOutput`] containing categorized -/// findings or an error if linting fails. -pub trait Linter: Send + Sync + Clone { - type Language: Language; - type Lint: Lint + Ord; - type LinterError: Error + Send + Sync + 'static; - - fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; -} - -pub struct ProjectLinter -where - L: Linter, -{ - pub linter: L, -} - -impl ProjectLinter -where - L: Linter, -{ - pub fn new(linter: L) -> Self { - Self { linter } - } - - pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { - Ok(self.linter.lint(input)?) - } -} - -#[derive(Default)] -pub struct LinterOutput(pub BTreeMap>); - -impl LinterOutput { - pub fn new() -> Self { - Self(BTreeMap::new()) - } -} - -impl Deref for LinterOutput { - type Target = BTreeMap>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for LinterOutput { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Extend<(L::Lint, Vec)> for LinterOutput { - fn extend)>>(&mut self, iter: T) { - for (lint, findings) in iter { - self.0.entry(lint).or_default().extend(findings); - } - } -} - -impl fmt::Display for LinterOutput { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f)?; - - for (lint, locations) in &self.0 { - let severity = lint.severity(); - let name = lint.name(); - let description = lint.description(); - - let mut file_contents = HashMap::new(); - - for location in locations { - let file_content = - file_contents.entry(location.file.clone()).or_insert_with(|| { - std::fs::read_to_string(&location.file) - .expect("Could not read file for source location") - }); - - let ((start_line, start_column), (end_line, end_column)) = - match location.location(file_content) { - Some(pos) => pos, - None => continue, - }; - - let max_line_number_width = start_line.to_string().len(); - - writeln!( - f, - "{}: {}: {}", - severity, - Paint::white(name).bold(), - Paint::white(description).bold() - )?; - writeln!( - f, - "{} {}:{}:{}", - Paint::blue(" -->").bold(), - location.file.display(), - start_line, - start_column - )?; - writeln!( - f, - "{:width$}{}", - "", - Paint::blue("|").bold(), - width = max_line_number_width + 1 - )?; - - let lines = file_content.lines().collect::>(); - for line_number in start_line..=end_line { - let line = lines.get(line_number - 1).unwrap_or(&""); - - writeln!( - f, - "{:>width$} {} {}", - if line_number == start_line { - line_number.to_string() - } else { - String::new() - }, - Paint::blue("|").bold(), - line, - width = max_line_number_width - )?; - - if line_number == start_line { - let caret = severity.color(&"^".repeat(end_column - start_column)); - writeln!( - f, - "{:width$}{} {}{}", - "", - Paint::blue("|").bold(), - " ".repeat(start_column - 1), - caret, - width = max_line_number_width + 1 - )?; - } - } - - if let Some(url) = lint.url() { - writeln!( - f, - "{:width$}{} {} {} {}", - "", - Paint::blue("=").bold(), - Paint::white("help:").bold(), - Paint::white("for further information visit"), - url, - width = max_line_number_width + 1 - )?; - } - - writeln!(f)?; - } - } - - Ok(()) - } -} - -pub trait Lint: Hash { - fn name(&self) -> &'static str; - fn description(&self) -> &'static str; - fn url(&self) -> Option<&'static str>; - fn severity(&self) -> Severity; -} - -#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] -pub enum Severity { - High, - Med, - Low, - Info, - Gas, -} - -impl Severity { - pub fn color(&self, message: &str) -> String { - match self { - Self::High => Paint::red(message).bold().to_string(), - Self::Med => Paint::rgb(message, 255, 135, 61).bold().to_string(), - Self::Low => Paint::yellow(message).bold().to_string(), - Self::Info => Paint::cyan(message).bold().to_string(), - Self::Gas => Paint::green(message).bold().to_string(), - } - } -} - -impl fmt::Display for Severity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let colored = match self { - Self::High => self.color("High"), - Self::Med => self.color("Med"), - Self::Low => self.color("Low"), - Self::Info => self.color("Info"), - Self::Gas => self.color("Gas"), - }; - write!(f, "{colored}") - } -} - -/// Represents the location of a specific AST node in the specified `file`. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct SourceLocation { - pub file: PathBuf, - pub span: Span, -} - -impl SourceLocation { - pub fn new(file: PathBuf, span: Span) -> Self { - Self { file, span } - } - - /// Find the start and end position of the span in the file content. - pub fn location(&self, file_content: &str) -> Option<((usize, usize), (usize, usize))> { - let lo = self.span.lo().0 as usize; - let hi = self.span.hi().0 as usize; - - // Ensure offsets are valid - if lo > file_content.len() || hi > file_content.len() || lo > hi { - return None; - } - - let mut offset = 0; - let mut start_line = 0; - let mut start_column = 0; - - for (line_number, line) in file_content.lines().enumerate() { - let line_length = line.len() + 1; - - // Check start position - if offset <= lo && lo < offset + line_length { - start_line = line_number + 1; - start_column = lo - offset + 1; - } - - // Check end position - if offset <= hi && hi < offset + line_length { - // Return if both positions are found - return Some(((start_line, start_column), (line_number + 1, hi - offset + 1))); - } - - offset += line_length; - } - - None - } -} diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs new file mode 100644 index 0000000000000..91f27a8cf7c8a --- /dev/null +++ b/crates/lint/src/linter.rs @@ -0,0 +1,275 @@ +use clap::ValueEnum; +use core::fmt; +use foundry_compilers::Language; +use solar_ast::ast::Span; +use std::{ + collections::{BTreeMap, HashMap}, + error::Error, + hash::Hash, + ops::{Deref, DerefMut}, + path::PathBuf, +}; +use yansi::Paint; + +/// Trait representing a generic linter for analyzing and reporting issues in smart contract source +/// code files. A linter can be implemented for any smart contract language supported by Foundry. +/// +/// # Type Parameters +/// +/// - `Language`: Represents the target programming language. Must implement the [`Language`] trait. +/// - `Lint`: Represents the types of lints performed by the linter. Must implement the [`Lint`] +/// trait. +/// - `LinterError`: Represents errors that can occur during the linting process. +/// +/// # Required Methods +/// +/// - `lint`: Scans the provided source files and returns a [`LinterOutput`] containing categorized +/// findings or an error if linting fails. +pub trait Linter: Send + Sync + Clone { + type Language: Language; + type Lint: Lint + Ord; + type LinterError: Error + Send + Sync + 'static; + + fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; +} + +pub struct ProjectLinter +where + L: Linter, +{ + pub linter: L, +} + +impl ProjectLinter +where + L: Linter, +{ + pub fn new(linter: L) -> Self { + Self { linter } + } + + pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { + Ok(self.linter.lint(input)?) + } +} + +#[derive(Default)] +pub struct LinterOutput(pub BTreeMap>); + +impl LinterOutput { + pub fn new() -> Self { + Self(BTreeMap::new()) + } +} + +impl Deref for LinterOutput { + type Target = BTreeMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for LinterOutput { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Extend<(L::Lint, Vec)> for LinterOutput { + fn extend)>>(&mut self, iter: T) { + for (lint, findings) in iter { + self.0.entry(lint).or_default().extend(findings); + } + } +} + +impl fmt::Display for LinterOutput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f)?; + + for (lint, locations) in &self.0 { + let severity = lint.severity(); + let name = lint.name(); + let description = lint.description(); + + let mut file_contents = HashMap::new(); + + for location in locations { + let file_content = + file_contents.entry(location.file.clone()).or_insert_with(|| { + std::fs::read_to_string(&location.file) + .expect("Could not read file for source location") + }); + + let ((start_line, start_column), (end_line, end_column)) = + match location.location(file_content) { + Some(pos) => pos, + None => continue, + }; + + let max_line_number_width = start_line.to_string().len(); + + writeln!( + f, + "{}: {}: {}", + severity, + Paint::white(name).bold(), + Paint::white(description).bold() + )?; + writeln!( + f, + "{} {}:{}:{}", + Paint::blue(" -->").bold(), + location.file.display(), + start_line, + start_column + )?; + writeln!( + f, + "{:width$}{}", + "", + Paint::blue("|").bold(), + width = max_line_number_width + 1 + )?; + + let lines = file_content.lines().collect::>(); + for line_number in start_line..=end_line { + let line = lines.get(line_number - 1).unwrap_or(&""); + + writeln!( + f, + "{:>width$} {} {}", + if line_number == start_line { + line_number.to_string() + } else { + String::new() + }, + Paint::blue("|").bold(), + line, + width = max_line_number_width + )?; + + if line_number == start_line { + let caret = severity.color(&"^".repeat(end_column - start_column)); + writeln!( + f, + "{:width$}{} {}{}", + "", + Paint::blue("|").bold(), + " ".repeat(start_column - 1), + caret, + width = max_line_number_width + 1 + )?; + } + } + + if let Some(url) = lint.url() { + writeln!( + f, + "{:width$}{} {} {} {}", + "", + Paint::blue("=").bold(), + Paint::white("help:").bold(), + Paint::white("for further information visit"), + url, + width = max_line_number_width + 1 + )?; + } + + writeln!(f)?; + } + } + + Ok(()) + } +} + +pub trait Lint: Hash { + fn name(&self) -> &'static str; + fn description(&self) -> &'static str; + fn url(&self) -> Option<&'static str>; + fn severity(&self) -> Severity; +} + +#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum Severity { + High, + Med, + Low, + Info, + Gas, +} + +impl Severity { + pub fn color(&self, message: &str) -> String { + match self { + Self::High => Paint::red(message).bold().to_string(), + Self::Med => Paint::rgb(message, 255, 135, 61).bold().to_string(), + Self::Low => Paint::yellow(message).bold().to_string(), + Self::Info => Paint::cyan(message).bold().to_string(), + Self::Gas => Paint::green(message).bold().to_string(), + } + } +} + +impl fmt::Display for Severity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let colored = match self { + Self::High => self.color("High"), + Self::Med => self.color("Med"), + Self::Low => self.color("Low"), + Self::Info => self.color("Info"), + Self::Gas => self.color("Gas"), + }; + write!(f, "{colored}") + } +} + +/// Represents the location of a specific AST node in the specified `file`. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SourceLocation { + pub file: PathBuf, + pub span: Span, +} + +impl SourceLocation { + pub fn new(file: PathBuf, span: Span) -> Self { + Self { file, span } + } + + /// Find the start and end position of the span in the file content. + pub fn location(&self, file_content: &str) -> Option<((usize, usize), (usize, usize))> { + let lo = self.span.lo().0 as usize; + let hi = self.span.hi().0 as usize; + + // Ensure offsets are valid + if lo > file_content.len() || hi > file_content.len() || lo > hi { + return None; + } + + let mut offset = 0; + let mut start_line = 0; + let mut start_column = 0; + + for (line_number, line) in file_content.lines().enumerate() { + let line_length = line.len() + 1; + + // Check start position + if offset <= lo && lo < offset + line_length { + start_line = line_number + 1; + start_column = lo - offset + 1; + } + + // Check end position + if offset <= hi && hi < offset + line_length { + // Return if both positions are found + return Some(((start_line, start_column), (line_number + 1, hi - offset + 1))); + } + + offset += line_length; + } + + None + } +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 6f7057929877d..08aab64204704 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -17,7 +17,7 @@ use solar_ast::{ use solar_interface::{ColorChoice, Session, Span}; use thiserror::Error; -use crate::{Lint, Linter, LinterOutput, Severity, SourceLocation}; +use crate::linter::{Lint, Linter, LinterOutput, Severity, SourceLocation}; /// Linter implementation to analyze Solidity source code responsible for identifying /// vulnerabilities gas optimizations, and best practices. From c27357543934144e6fe0eec60e286f3f9c62b0e1 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Wed, 1 Jan 2025 21:06:27 -0500 Subject: [PATCH 067/107] fix info lints --- crates/lint/src/sol/info.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 2b118e3e8327a..2c6cdc26eb545 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -11,7 +11,8 @@ impl<'ast> Visit<'ast> for VariableMixedCase { fn visit_variable_definition(&mut self, var: &'ast VariableDefinition<'ast>) { if var.mutability.is_none() { if let Some(name) = var.name { - if !is_mixed_case(name.as_str()) { + let name = name.as_str(); + if !is_mixed_case(name) && name.len() > 1 { self.results.push(var.span); } } @@ -26,7 +27,8 @@ impl<'ast> Visit<'ast> for ScreamingSnakeCase { if let Some(mutability) = var.mutability { if mutability.is_constant() || mutability.is_immutable() { if let Some(name) = var.name { - if !is_screaming_snake_case(name.as_str()) { + let name = name.as_str(); + if !is_screaming_snake_case(name) && name.len() > 1 { self.results.push(var.span); } } @@ -38,7 +40,9 @@ impl<'ast> Visit<'ast> for ScreamingSnakeCase { impl<'ast> Visit<'ast> for StructPascalCase { fn visit_item_struct(&mut self, strukt: &'ast ItemStruct<'ast>) { - if !is_pascal_case(strukt.name.as_str()) { + let name = strukt.name.as_str(); + + if !is_pascal_case(name) && name.len() > 1 { self.results.push(strukt.name.span); } From 682d52298360888f53c34b486a8e7bf301619dd4 Mon Sep 17 00:00:00 2001 From: 0xKitsune <77890308+0xKitsune@users.noreply.github.com> Date: Sun, 19 Jan 2025 01:13:04 -0500 Subject: [PATCH 068/107] Use Solar daignostics instead of `LinterOutput` (#6) * use solar diagnostics, remove unneeded types * update diagnostic emission * clippy * set track daignostics to false * display help message --- crates/forge/bin/cmd/lint.rs | 9 +- crates/lint/src/linter.rs | 214 +---------------------------------- crates/lint/src/sol/mod.rs | 61 ++++------ 3 files changed, 32 insertions(+), 252 deletions(-) diff --git a/crates/forge/bin/cmd/lint.rs b/crates/forge/bin/cmd/lint.rs index d84827dc878a3..f26544ca3a7b7 100644 --- a/crates/forge/bin/cmd/lint.rs +++ b/crates/forge/bin/cmd/lint.rs @@ -1,7 +1,7 @@ use clap::{Parser, ValueHint}; use eyre::Result; use forge_lint::{ - linter::{ProjectLinter, Severity}, + linter::{Linter, Severity}, sol::SolidityLinter, }; use foundry_cli::utils::LoadConfig; @@ -62,15 +62,12 @@ impl LintArgs { std::process::exit(0); } - let linter = if project.compiler.solc.is_some() { - SolidityLinter::new().with_severity(self.severity) + if project.compiler.solc.is_some() { + SolidityLinter::new().with_severity(self.severity).lint(&sources)?; } else { todo!("Linting not supported for this language"); }; - let output = ProjectLinter::new(linter).lint(&sources)?; - sh_println!("{}", &output)?; - Ok(()) } } diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index a38892c27bf12..0c42ffaa4ba2d 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -1,14 +1,7 @@ use clap::ValueEnum; use core::fmt; use foundry_compilers::Language; -use solar_ast::Span; -use std::{ - collections::{BTreeMap, HashMap}, - error::Error, - hash::Hash, - ops::{Deref, DerefMut}, - path::PathBuf, -}; +use std::{error::Error, hash::Hash, path::PathBuf}; use yansi::Paint; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source @@ -23,6 +16,7 @@ use yansi::Paint; /// /// # Required Methods /// +/// TODO: update this /// - `lint`: Scans the provided source files and returns a [`LinterOutput`] containing categorized /// findings or an error if linting fails. pub trait Linter: Send + Sync + Clone { @@ -30,165 +24,13 @@ pub trait Linter: Send + Sync + Clone { type Lint: Lint + Ord; type LinterError: Error + Send + Sync + 'static; - fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError>; -} - -pub struct ProjectLinter -where - L: Linter, -{ - pub linter: L, -} - -impl ProjectLinter -where - L: Linter, -{ - pub fn new(linter: L) -> Self { - Self { linter } - } - - pub fn lint(self, input: &[PathBuf]) -> eyre::Result> { - Ok(self.linter.lint(input)?) - } -} - -#[derive(Default)] -pub struct LinterOutput(pub BTreeMap>); - -impl LinterOutput { - pub fn new() -> Self { - Self(BTreeMap::new()) - } -} - -impl Deref for LinterOutput { - type Target = BTreeMap>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for LinterOutput { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Extend<(L::Lint, Vec)> for LinterOutput { - fn extend)>>(&mut self, iter: T) { - for (lint, findings) in iter { - self.0.entry(lint).or_default().extend(findings); - } - } -} - -impl fmt::Display for LinterOutput { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f)?; - - for (lint, locations) in &self.0 { - let severity = lint.severity(); - let name = lint.name(); - let description = lint.description(); - - let mut file_contents = HashMap::new(); - - for location in locations { - let file_content = - file_contents.entry(location.file.clone()).or_insert_with(|| { - std::fs::read_to_string(&location.file) - .expect("Could not read file for source location") - }); - - let ((start_line, start_column), (end_line, end_column)) = - match location.location(file_content) { - Some(pos) => pos, - None => continue, - }; - - let max_line_number_width = start_line.to_string().len(); - - writeln!( - f, - "{}: {}: {}", - severity, - Paint::white(name).bold(), - Paint::white(description).bold() - )?; - writeln!( - f, - "{} {}:{}:{}", - Paint::blue(" -->").bold(), - location.file.display(), - start_line, - start_column - )?; - writeln!( - f, - "{:width$}{}", - "", - Paint::blue("|").bold(), - width = max_line_number_width + 1 - )?; - - let lines = file_content.lines().collect::>(); - for line_number in start_line..=end_line { - let line = lines.get(line_number - 1).unwrap_or(&""); - - writeln!( - f, - "{:>width$} {} {}", - if line_number == start_line { - line_number.to_string() - } else { - String::new() - }, - Paint::blue("|").bold(), - line, - width = max_line_number_width - )?; - - if line_number == start_line { - let caret = severity.color(&"^".repeat(end_column - start_column)); - writeln!( - f, - "{:width$}{} {}{}", - "", - Paint::blue("|").bold(), - " ".repeat(start_column - 1), - caret, - width = max_line_number_width + 1 - )?; - } - } - - if let Some(url) = lint.url() { - writeln!( - f, - "{:width$}{} {} {} {}", - "", - Paint::blue("=").bold(), - Paint::white("help:").bold(), - Paint::white("for further information visit"), - url, - width = max_line_number_width + 1 - )?; - } - - writeln!(f)?; - } - } - - Ok(()) - } + fn lint(&self, input: &[PathBuf]) -> Result<(), Self::LinterError>; } pub trait Lint: Hash { fn name(&self) -> &'static str; fn description(&self) -> &'static str; - fn url(&self) -> Option<&'static str>; + fn help(&self) -> Option<&'static str>; fn severity(&self) -> Severity; } @@ -225,51 +67,3 @@ impl fmt::Display for Severity { write!(f, "{colored}") } } - -/// Represents the location of a specific AST node in the specified `file`. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct SourceLocation { - pub file: PathBuf, - pub span: Span, -} - -impl SourceLocation { - pub fn new(file: PathBuf, span: Span) -> Self { - Self { file, span } - } - - /// Find the start and end position of the span in the file content. - pub fn location(&self, file_content: &str) -> Option<((usize, usize), (usize, usize))> { - let lo = self.span.lo().0 as usize; - let hi = self.span.hi().0 as usize; - - // Ensure offsets are valid - if lo > file_content.len() || hi > file_content.len() || lo > hi { - return None; - } - - let mut offset = 0; - let mut start_line = 0; - let mut start_column = 0; - - for (line_number, line) in file_content.lines().enumerate() { - let line_length = line.len() + 1; - - // Check start position - if offset <= lo && lo < offset + line_length { - start_line = line_number + 1; - start_column = lo - offset + 1; - } - - // Check end position - if offset <= hi && hi < offset + line_length { - // Return if both positions are found - return Some(((start_line, start_column), (line_number + 1, hi - offset + 1))); - } - - offset += line_length; - } - - None - } -} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index fd5121c412402..506f48d7022b9 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -12,10 +12,10 @@ use std::{ use foundry_compilers::solc::SolcLanguage; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena, SourceUnit}; -use solar_interface::{ColorChoice, Session, Span}; +use solar_interface::{diagnostics::Level, Session, Span}; use thiserror::Error; -use crate::linter::{Lint, Linter, LinterOutput, Severity, SourceLocation}; +use crate::linter::{Lint, Linter, Severity}; /// Linter implementation to analyze Solidity source code responsible for identifying /// vulnerabilities gas optimizations, and best practices. @@ -46,8 +46,8 @@ impl Linter for SolidityLinter { type Lint = SolLint; type LinterError = SolLintError; - fn lint(&self, input: &[PathBuf]) -> Result, Self::LinterError> { - let all_findings = input + fn lint(&self, input: &[PathBuf]) -> Result<(), Self::LinterError> { + let _ = input .into_par_iter() .map(|file| { let mut lints = if let Some(severity) = &self.severity { @@ -56,46 +56,35 @@ impl Linter for SolidityLinter { SolLint::all() }; - // Initialize session and parsing environment - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); + let mut sess = Session::builder().with_stderr_emitter().build(); + sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); + let arena = Arena::new(); - // Enter the session context for this thread let _ = sess.enter(|| -> solar_interface::Result<()> { let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; let ast = parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); - // Run all lints on the parsed AST and collect findings + // Run all lints on the parsed AST for lint in lints.iter_mut() { - lint.lint(&ast); + for span in lint.lint(&ast) { + sess.dcx + .diag::<()>( + Level::Warning, + format!("{}: {}", lint.severity(), lint.description()), + ) + .span(span) + .help(lint.help().unwrap_or_default()) + .emit() + } } - Ok(()) }); - - (file.to_owned(), lints) }) - .collect::)>>(); - - let mut output = LinterOutput::new(); - for (file, lints) in all_findings { - for lint in lints { - let source_locations = lint - .results() - .iter() - .map(|span| SourceLocation::new(file.clone(), *span)) - .collect::>(); - - if source_locations.is_empty() { - continue; - } - - output.insert(lint, source_locations); - } - } + .collect::>(); - Ok(output) + Ok(()) } } @@ -116,7 +105,7 @@ pub enum SolLintError {} /// - `$description`: A short description of the lint. /// - `$url`: URL providing additional information about the lint or best practices. macro_rules! declare_sol_lints { - ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr, $url:expr)),* $(,)?) => { + ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr, $help:expr)),* $(,)?) => { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SolLint { $( @@ -202,12 +191,12 @@ macro_rules! declare_sol_lints { } } - fn url(&self) -> Option<&'static str> { + fn help(&self) -> Option<&'static str> { match self { $( SolLint::$name(_) => { - if !$url.is_empty() { - Some($url) + if !$help.is_empty() { + Some($help) } else { None } @@ -247,7 +236,7 @@ declare_sol_lints!( // TODO: FunctionMixedCase // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", "https://placeholder.xyz"), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", ""), // TODO: PackStorageVariables // TODO: PackStructs // TODO: UseConstantVariable From 03e643d26c5f7fa16e9477278b7f0c6cee31f632 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 19 Jan 2025 01:40:56 -0500 Subject: [PATCH 069/107] set level according to severity --- Cargo.lock | 2 +- crates/lint/src/sol/mod.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8974e9f3f625e..6e62f89d3167a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3453,7 +3453,7 @@ dependencies = [ "solar-ast", "solar-interface", "solar-parse", - "thiserror 2.0.9", + "thiserror 2.0.11", "yansi", ] diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 506f48d7022b9..a4cab7425a4ed 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -69,9 +69,14 @@ impl Linter for SolidityLinter { // Run all lints on the parsed AST for lint in lints.iter_mut() { for span in lint.lint(&ast) { + let level = match lint.severity() { + Severity::High | Severity::Med | Severity::Low => Level::Warning, + Severity::Info | Severity::Gas => Level::Note, + }; + sess.dcx .diag::<()>( - Level::Warning, + level, format!("{}: {}", lint.severity(), lint.description()), ) .span(span) From f04acba867aacb963dcff6e505582dc649476665 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 19 Jan 2025 12:16:59 -0500 Subject: [PATCH 070/107] update descriptions to be more concise --- crates/lint/src/linter.rs | 7 ++- crates/lint/src/sol/mod.rs | 91 +++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index 0c42ffaa4ba2d..747284bbd9412 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -16,9 +16,8 @@ use yansi::Paint; /// /// # Required Methods /// -/// TODO: update this -/// - `lint`: Scans the provided source files and returns a [`LinterOutput`] containing categorized -/// findings or an error if linting fails. +/// - `lint`: Scans the provided source files emitting a daignostic for lints found. +/// Returns an error if linting fails. pub trait Linter: Send + Sync + Clone { type Language: Language; type Lint: Lint + Ord; @@ -28,7 +27,7 @@ pub trait Linter: Send + Sync + Clone { } pub trait Lint: Hash { - fn name(&self) -> &'static str; + fn id(&self) -> &'static str; fn description(&self) -> &'static str; fn help(&self) -> Option<&'static str>; fn severity(&self) -> Severity; diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index a4cab7425a4ed..a43f4ebf00fd5 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -9,13 +9,13 @@ use std::{ path::PathBuf, }; +use crate::linter::{Lint, Linter, Severity}; use foundry_compilers::solc::SolcLanguage; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena, SourceUnit}; use solar_interface::{diagnostics::Level, Session, Span}; use thiserror::Error; - -use crate::linter::{Lint, Linter, Severity}; +use yansi::Paint; /// Linter implementation to analyze Solidity source code responsible for identifying /// vulnerabilities gas optimizations, and best practices. @@ -63,8 +63,7 @@ impl Linter for SolidityLinter { let _ = sess.enter(|| -> solar_interface::Result<()> { let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - let ast = - parser.parse_file().map_err(|e| e.emit()).expect("Failed to parse file"); + let ast = parser.parse_file().map_err(|e| e.emit())?; // Run all lints on the parsed AST for lint in lints.iter_mut() { @@ -77,7 +76,7 @@ impl Linter for SolidityLinter { sess.dcx .diag::<()>( level, - format!("{}: {}", lint.severity(), lint.description()), + format!("{}: {}", lint.severity(), lint.description().bold()), ) .span(span) .help(lint.help().unwrap_or_default()) @@ -104,17 +103,17 @@ pub enum SolLintError {} /// # Parameters /// /// Each lint is defined as a tuple with the following fields: -/// - `$name`: Identitifier used for the struct and enum variant created for the lint. +/// - `$id`: Identitifier used as the struct and enum variant created for the lint. /// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). -/// - `$lint_name`: A string identifier for the lint used during reporting. /// - `$description`: A short description of the lint. -/// - `$url`: URL providing additional information about the lint or best practices. +/// - `$help`: Link to additional information about the lint or best practices. +/// - `$str_id`: A unique identifier used to reference a specific lint during configuration. macro_rules! declare_sol_lints { - ($(($name:ident, $severity:expr, $lint_name:expr, $description:expr, $help:expr)),* $(,)?) => { + ($(($id:ident, $severity:expr, $str_id:expr, $description:expr, $help:expr)),* $(,)?) => { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SolLint { $( - $name($name), + $id($id), )* } @@ -122,7 +121,7 @@ macro_rules! declare_sol_lints { pub fn all() -> Vec { vec![ $( - SolLint::$name($name::new()), + SolLint::$id($id::new()), )* ] } @@ -130,7 +129,7 @@ macro_rules! declare_sol_lints { pub fn results(&self) -> &[Span] { match self { $( - SolLint::$name(lint) => &lint.results, + SolLint::$id(lint) => &lint.results, )* } } @@ -145,7 +144,7 @@ macro_rules! declare_sol_lints { pub fn lint(&mut self, source_unit: &SourceUnit<'_>) -> Vec { match self { $( - SolLint::$name(lint) => { + SolLint::$id(lint) => { lint.visit_source_unit(source_unit); lint.results.clone() }, @@ -159,7 +158,7 @@ macro_rules! declare_sol_lints { fn visit_source_unit(&mut self, source_unit: &SourceUnit<'ast>) -> ControlFlow { match self { $( - SolLint::$name(lint) => lint.visit_source_unit(source_unit), + SolLint::$id(lint) => lint.visit_source_unit(source_unit), )* } } @@ -167,31 +166,32 @@ macro_rules! declare_sol_lints { impl Hash for SolLint { fn hash(&self, state: &mut H) { - self.name().hash(state); + self.id().hash(state); } } + impl Lint for SolLint { - fn name(&self) -> &'static str { + fn description(&self) -> &'static str { match self { $( - SolLint::$name(_) => $lint_name, + SolLint::$id(_) => $description, )* } } - fn description(&self) -> &'static str { + fn severity(&self) -> Severity { match self { $( - SolLint::$name(_) => $description, + SolLint::$id(_) => $severity, )* } } - fn severity(&self) -> Severity { + fn id(&self) -> &'static str { match self { $( - SolLint::$name(_) => $severity, + SolLint::$id(_) => $str_id, )* } } @@ -199,7 +199,7 @@ macro_rules! declare_sol_lints { fn help(&self) -> Option<&'static str> { match self { $( - SolLint::$name(_) => { + SolLint::$id(_) => { if !$help.is_empty() { Some($help) } else { @@ -213,11 +213,11 @@ macro_rules! declare_sol_lints { $( #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] - pub struct $name { + pub struct $id { pub results: Vec, } - impl $name { + impl $id { pub fn new() -> Self { Self { results: Vec::new() } } @@ -228,20 +228,49 @@ macro_rules! declare_sol_lints { declare_sol_lints!( //High - (IncorrectShift, Severity::High, "incorrect-shift", "The order of args in a shift operation is incorrect.", ""), + ( + IncorrectShift, + Severity::High, + "incorrect-shift", + "The order of args in a shift operation is incorrect", + "" + ), // Med - - (DivideBeforeMultiply, Severity::Med, "divide-before-multiply", "Multiplication should occur before division to avoid loss of precision.", ""), + ( + DivideBeforeMultiply, + Severity::Med, + "divide-before-multiply", + "Multiplication should occur before division to avoid loss of precision", + "" + ), // Low // Info - (VariableMixedCase, Severity::Info, "variable-mixed-case", "Variables should follow `camelCase` naming conventions unless they are constants or immutables.", ""), - (ScreamingSnakeCase, Severity::Info, "screaming-snake-case", "Constants and immutables should be named with all capital letters with underscores separating words.", "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names"), - (StructPascalCase, Severity::Info, "struct-pascal-case", "Structs should be named using PascalCase. Examples: MyCoin, Position", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names"), + ( + VariableMixedCase, + Severity::Info, + "variable-mixed-case", + "Mutable variables should use mixedCase", + "" + ), + ( + ScreamingSnakeCase, + Severity::Info, + "screaming-snake-case", + "Constants and immutables should use SCREAMING_SNAKE_CASE", + "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names" + ), + ( + StructPascalCase, + Severity::Info, + "struct-pascal-case", + "Structs should use PascalCase.", + "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names" + ), // TODO: FunctionMixedCase // Gas Optimizations - (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hashing via keccak256 can be done with inline assembly to save gas.", ""), + (AsmKeccak256, Severity::Gas, "asm-keccak256", "Hash via inline assembly to save gas", ""), // TODO: PackStorageVariables // TODO: PackStructs // TODO: UseConstantVariable From 11b2c192f49463d8e999930e6cf4aa539fd33c58 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xkitsune@protonmail.com> Date: Sun, 19 Jan 2025 13:05:02 -0500 Subject: [PATCH 071/107] removed LinterError from lint trait --- crates/lint/src/linter.rs | 7 +--- crates/lint/src/sol/mod.rs | 85 ++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index 747284bbd9412..57e012bffa5b1 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -1,7 +1,7 @@ use clap::ValueEnum; use core::fmt; use foundry_compilers::Language; -use std::{error::Error, hash::Hash, path::PathBuf}; +use std::{hash::Hash, path::PathBuf}; use yansi::Paint; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source @@ -12,18 +12,15 @@ use yansi::Paint; /// - `Language`: Represents the target programming language. Must implement the [`Language`] trait. /// - `Lint`: Represents the types of lints performed by the linter. Must implement the [`Lint`] /// trait. -/// - `LinterError`: Represents errors that can occur during the linting process. /// /// # Required Methods /// /// - `lint`: Scans the provided source files emitting a daignostic for lints found. -/// Returns an error if linting fails. pub trait Linter: Send + Sync + Clone { type Language: Language; type Lint: Lint + Ord; - type LinterError: Error + Send + Sync + 'static; - fn lint(&self, input: &[PathBuf]) -> Result<(), Self::LinterError>; + fn lint(&self, input: &[PathBuf]); } pub trait Lint: Hash { diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index a43f4ebf00fd5..862b62cd2e1e4 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -13,7 +13,10 @@ use crate::linter::{Lint, Linter, Severity}; use foundry_compilers::solc::SolcLanguage; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena, SourceUnit}; -use solar_interface::{diagnostics::Level, Session, Span}; +use solar_interface::{ + diagnostics::{ErrorGuaranteed, Level}, + Session, Span, +}; use thiserror::Error; use yansi::Paint; @@ -44,51 +47,45 @@ impl SolidityLinter { impl Linter for SolidityLinter { type Language = SolcLanguage; type Lint = SolLint; - type LinterError = SolLintError; - - fn lint(&self, input: &[PathBuf]) -> Result<(), Self::LinterError> { - let _ = input - .into_par_iter() - .map(|file| { - let mut lints = if let Some(severity) = &self.severity { - SolLint::with_severity(severity.to_owned()) - } else { - SolLint::all() - }; - - let mut sess = Session::builder().with_stderr_emitter().build(); - sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); - - let arena = Arena::new(); - let _ = sess.enter(|| -> solar_interface::Result<()> { - let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; - let ast = parser.parse_file().map_err(|e| e.emit())?; - - // Run all lints on the parsed AST - for lint in lints.iter_mut() { - for span in lint.lint(&ast) { - let level = match lint.severity() { - Severity::High | Severity::Med | Severity::Low => Level::Warning, - Severity::Info | Severity::Gas => Level::Note, - }; - - sess.dcx - .diag::<()>( - level, - format!("{}: {}", lint.severity(), lint.description().bold()), - ) - .span(span) - .help(lint.help().unwrap_or_default()) - .emit() - } + fn lint(&self, input: &[PathBuf]) { + let _ = input.into_par_iter().map(|file| { + let mut lints = if let Some(severity) = &self.severity { + SolLint::with_severity(severity.to_owned()) + } else { + SolLint::all() + }; + + let mut sess = Session::builder().with_stderr_emitter().build(); + sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); + + let arena = Arena::new(); + + let _ = sess.enter(|| -> Result<(), ErrorGuaranteed> { + let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + let ast = parser.parse_file().map_err(|e| e.emit())?; + + // Run all lints on the parsed AST + for lint in lints.iter_mut() { + for span in lint.lint(&ast) { + let level = match lint.severity() { + Severity::High | Severity::Med | Severity::Low => Level::Warning, + Severity::Info | Severity::Gas => Level::Note, + }; + + sess.dcx + .diag::<()>( + level, + format!("{}: {}", lint.severity(), lint.description().bold()), + ) + .span(span) + .help(lint.help().unwrap_or_default()) + .emit() } - Ok(()) - }); - }) - .collect::>(); - - Ok(()) + } + Ok(()) + }); + }); } } From 776db4349569beb97408e031a6d7958308dbee5c Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 29 Apr 2025 00:37:34 +0200 Subject: [PATCH 072/107] early pass + tests --- crates/forge/src/cmd/lint.rs | 36 ++- crates/lint/src/linter.rs | 156 ++++++++++--- crates/lint/src/sol/gas.rs | 49 ++-- crates/lint/src/sol/high.rs | 52 ++++- crates/lint/src/sol/info.rs | 171 +++++++------- crates/lint/src/sol/med.rs | 54 ++--- crates/lint/src/sol/mod.rs | 248 ++++++++++++++++----- crates/lint/testdata/FunctionMixedCase.sol | 14 ++ crates/lint/testdata/IncorrectShift.sol | 38 ++++ 9 files changed, 574 insertions(+), 244 deletions(-) create mode 100644 crates/lint/testdata/FunctionMixedCase.sol create mode 100644 crates/lint/testdata/IncorrectShift.sol diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index 3311be74efbef..216d7756ed1a8 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -2,7 +2,7 @@ use clap::{Parser, ValueHint}; use eyre::Result; use forge_lint::{ linter::{Linter, Severity}, - sol::SolidityLinter, + sol::{SolLint, SolLintError, SolidityLinter}, }; use foundry_cli::utils::LoadConfig; use foundry_config::impl_figment_convert_basic; @@ -31,6 +31,18 @@ pub struct LintArgs { /// Supported values: `high`, `med`, `low`, `info`, `gas`. #[arg(long, value_name = "SEVERITY", num_args(1..))] severity: Option>, + + /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). + /// + /// Cannot be used with --deny-lint. + #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..), conflicts_with = "exclude_lint")] + include_lint: Option>, + + /// Deny specific lints based on their ID (e.g., "function-mixed-case"). + /// + /// Cannot be used with --only-lint. + #[arg(long = "deny-lint", value_name = "LINT_ID", num_args(1..), conflicts_with = "include_lint")] + exclude_lint: Option>, } impl_figment_convert_basic!(LintArgs); @@ -63,7 +75,27 @@ impl LintArgs { } if project.compiler.solc.is_some() { - SolidityLinter::new().with_severity(self.severity).lint(&sources); + let mut linter = SolidityLinter::new().with_severity(self.severity); + + // Resolve and apply included lints if provided + if let Some(ref include_ids) = self.include_lint { + let included_lints = include_ids + .iter() + .map(|id_str| SolLint::try_from(id_str.as_str())) + .collect::, SolLintError>>()?; + linter = linter.with_lints(Some(included_lints)); + } + + // Resolve and apply excluded lints if provided + if let Some(ref exclude_ids) = self.exclude_lint { + let excluded_lints = exclude_ids + .iter() + .map(|id_str| SolLint::try_from(id_str.as_str())) + .collect::, SolLintError>>()?; + linter = linter.without_lints(Some(excluded_lints)); + } + + linter.lint(&sources); } else { todo!("Linting not supported for this language"); }; diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index ef28a73e0f6e8..127fbeb792c2a 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -1,9 +1,12 @@ use clap::ValueEnum; -use solar_ast::{Expr, ItemStruct, Span, VariableDefinition}; -use solar_interface::{diagnostics::Level, Session}; use core::fmt; -use foundry_compilers::{artifacts::SourceUnit, Language}; -use std::{hash::Hash, marker::PhantomData, ops::ControlFlow, path::PathBuf}; +use foundry_compilers::Language; +use solar_ast::{visit::Visit, Expr, ItemFunction, ItemStruct, Span, VariableDefinition}; +use solar_interface::{ + diagnostics::{DiagBuilder, Level}, + Session, +}; +use std::{ops::ControlFlow, path::PathBuf}; use yansi::Paint; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source @@ -18,13 +21,20 @@ use yansi::Paint; /// # Required Methods /// /// - `lint`: Scans the provided source files emitting a daignostic for lints found. +pub trait Linter: Send + Sync + Clone { + type Language: Language; + type Lint: Lint; -#[derive(Debug, Clone, Copy)] -pub struct Lint { - pub id: &'static str, - pub description: &'static str, - pub help: Option<&'static str>, - pub severity: Severity, + fn lint(&self, input: &[PathBuf]); +} + +pub trait Lint { + fn id(&self) -> &'static str; + fn severity(&self) -> Severity; + fn description(&self) -> &'static str; + fn help(&self) -> Option<&'static str> { + None + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -48,6 +58,15 @@ impl Severity { } } +impl From for Level { + fn from(severity: Severity) -> Self { + match severity { + Severity::High | Severity::Med | Severity::Low => Self::Warning, + Severity::Info | Severity::Gas => Self::Note, + } + } +} + impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let colored = match self { @@ -61,30 +80,115 @@ impl fmt::Display for Severity { } } -pub struct LintContext<'s, 'ast> { +pub struct LintContext<'s> { pub sess: &'s Session, - _phantom: PhantomData<&'ast ()>, } -impl<'s, 'ast> LintContext<'s, 'ast> { +impl<'s> LintContext<'s> { pub fn new(sess: &'s Session) -> Self { - Self { sess, _phantom: PhantomData } + Self { sess } } // Helper method to emit diagnostics easily from passes - pub fn emit(&self, lint: &'static Lint, span: Span) { - let mut diag = self.sess.dcx.diag(lint.severity, format!("{}: {}", lint.id, lint.description)); - diag.span(span); - if let Some(help) = lint.help { - diag.help(help); - } - diag.emit(); + pub fn emit(&self, lint: &'static L, span: Span) { + let msg = format!("{}: {}", lint.id(), lint.description()); + let diag: DiagBuilder<'_, ()> = match lint.help() { + Some(help) => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(help), + None => self.sess.dcx.diag(lint.severity().into(), msg).span(span), + }; + + diag.emit(); + } +} + +/// Trait for lints that operate directly on the AST. +/// Its methods mirror `solar_ast::visit::Visit`, with the addition of `LintCotext`. +pub trait EarlyLintPass<'ast>: Send + Sync { + fn check_expr(&mut self, _ctx: &LintContext<'_>, _expr: &'ast Expr<'ast>) -> ControlFlow<()> { + ControlFlow::Continue(()) } + fn check_variable_definition( + &mut self, + _ctx: &LintContext<'_>, + _var: &'ast VariableDefinition<'ast>, + ) -> ControlFlow<()> { + ControlFlow::Continue(()) + } + fn check_item_struct( + &mut self, + _ctx: &LintContext<'_>, + _struct: &'ast ItemStruct<'ast>, + ) -> ControlFlow<()> { + ControlFlow::Continue(()) + } + fn check_item_function( + &mut self, + _ctx: &LintContext<'_>, + _func: &'ast ItemFunction<'ast>, + ) -> ControlFlow<()> { + ControlFlow::Continue(()) + } + + // TODO: Add methods for each required AST node type } -pub trait EarlyLintPass<'ast>: fmt::Debug + Send + Sync + Clone { - // TODO: Add methods for each required AST node type, mirroring `solar_ast::visit::Visit` method sigs + adding `LintContext` - fn check_expr(&mut self, _cx: &LintContext<'_, 'ast>, _expr: &'ast Expr<'ast>) -> ControlFlow<()> { ControlFlow::Continue(()) } - fn check_variable_definition(&mut self, _cx: &LintContext<'_, 'ast>, _var: &'ast VariableDefinition<'ast>) -> ControlFlow<()> { ControlFlow::Continue(()) } - fn check_item_struct(&mut self, _cx: &LintContext<'_, 'ast>, _struct: &'ast ItemStruct<'ast>) -> ControlFlow<()> { ControlFlow::Continue(()) } +/// Visitor struct for `EarlyLintPass`es +pub struct EarlyLintVisitor<'a, 's, 'ast> { + pub ctx: &'a LintContext<'s>, + pub passes: &'a mut [Box + 's>], +} + +impl<'a, 's, 'ast> Visit<'ast> for EarlyLintVisitor<'a, 's, 'ast> +where + 's: 'ast, +{ + type BreakValue = (); + + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow { + for pass in self.passes.iter_mut() { + if let ControlFlow::Break(_) = pass.check_expr(self.ctx, expr) { + return ControlFlow::Break(()); + } + } + self.walk_expr(expr) + } + + fn visit_variable_definition( + &mut self, + var: &'ast VariableDefinition<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + if let ControlFlow::Break(_) = pass.check_variable_definition(self.ctx, var) { + return ControlFlow::Break(()); + } + } + self.walk_variable_definition(var) + } + + fn visit_item_struct( + &mut self, + strukt: &'ast ItemStruct<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + if let ControlFlow::Break(_) = pass.check_item_struct(self.ctx, strukt) { + return ControlFlow::Break(()); + } + } + self.walk_item_struct(strukt) + } + + fn visit_item_function( + &mut self, + func: &'ast ItemFunction<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + if let ControlFlow::Break(_) = pass.check_item_function(self.ctx, func) { + return ControlFlow::Break(()); + } + } + self.walk_item_function(func) + } + + // TODO: Add methods for each required AST node type, mirroring `solar_ast::visit::Visit` method + // sigs + adding `LintContext` } diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 3c8d2c0cfbc1f..43c63e86b206f 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -1,53 +1,46 @@ use std::ops::ControlFlow; +use solar_ast::{Expr, ExprKind}; -use solar_ast::{visit::Visit, Expr, ExprKind}; +use crate::linter::EarlyLintPass; +use super::{AsmKeccak256, ASM_KECCACK256}; -use super::AsmKeccak256; - -impl<'ast> Visit<'ast> for AsmKeccak256 { - type BreakValue = (); - - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow { +impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { + fn check_expr( + &mut self, + ctx: &crate::linter::LintContext<'_>, + expr: &'ast Expr<'ast>, + ) -> ControlFlow<()> { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { if ident.name.as_str() == "keccak256" { - self.results.push(expr.span); + ctx.emit(&ASM_KECCACK256, expr.span); } } } - self.walk_expr(expr); ControlFlow::Continue(()) } } #[cfg(test)] mod test { - use solar_ast::{visit::Visit, Arena}; - use solar_interface::{ColorChoice, Session}; use std::path::Path; - use super::AsmKeccak256; + use super::*; + use crate::{linter::Lint, sol::SolidityLinter}; #[test] fn test_keccak256() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = Arena::new(); - - let mut parser = - solar_parse::Parser::from_file(&sess, &arena, Path::new("testdata/Keccak256.sol"))?; - - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit())?; - - let mut pattern = AsmKeccak256::default(); - pattern.visit_source_unit(&ast); + let linter = SolidityLinter::new() + .with_lints(Some(vec![ASM_KECCACK256])) + .with_buffer_emitter(true); - assert_eq!(pattern.results.len(), 2); + let emitted = + linter.lint_file(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", ASM_KECCACK256.id())).count(); + let notes = emitted.matches(&format!("note: {}", ASM_KECCACK256.id())).count(); - Ok(()) - }); + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 2, "Expected 2 notes"); Ok(()) } diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high.rs index b0e9e70a10ab8..a9535064be42a 100644 --- a/crates/lint/src/sol/high.rs +++ b/crates/lint/src/sol/high.rs @@ -1,14 +1,50 @@ use std::ops::ControlFlow; +use solar_ast::{BinOp, BinOpKind, Expr, ExprKind}; -use solar_ast::{visit::Visit, Expr}; +use super::{EarlyLintPass, IncorrectShift, LintContext, INCORRECT_SHIFT}; -use super::IncorrectShift; - -impl<'ast> Visit<'ast> for IncorrectShift { - type BreakValue = (); - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow { - // TODO: - self.walk_expr(expr); +impl<'ast> EarlyLintPass<'ast> for IncorrectShift { + fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) -> ControlFlow<()> { + if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Shl | BinOpKind::Shr, .. }, right_expr) = &expr.kind { + if contains_incorrect_shift(left_expr, right_expr) { + ctx.emit(&INCORRECT_SHIFT, expr.span); + } + } ControlFlow::Continue(()) } } + +// TODO: come up with a better heuristic. Treat initial impl as a PoC. +// Checks if the left operand is a literal and the right operand is not, indicating a potential reversed shift operation. +fn contains_incorrect_shift<'ast>(left_expr: &'ast Expr<'ast>, right_expr: &'ast Expr<'ast>) -> bool { + let is_left_literal = matches!(left_expr.kind, ExprKind::Lit(..)); + let is_right_not_literal = !matches!(right_expr.kind, ExprKind::Lit(..)); + + is_left_literal && is_right_not_literal +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use super::*; + use crate::{linter::Lint, sol::SolidityLinter}; + + #[test] + fn test_incorrect_shift() -> eyre::Result<()> { + let linter = SolidityLinter::new() + .with_lints(Some(vec![INCORRECT_SHIFT])) + .with_buffer_emitter(true); + + let emitted = + linter.lint_file(Path::new("testdata/IncorrectShift.sol")).unwrap().to_string(); + let warnings = + emitted.matches(&format!("warning: {}", INCORRECT_SHIFT.id())).count(); + let notes = emitted.matches(&format!("note: {}", INCORRECT_SHIFT.id())).count(); + + assert_eq!(warnings, 5, "Expected 5 warnings"); + assert_eq!(notes, 0, "Expected 0 notes"); + + Ok(()) + } +} diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 0edc27d9481df..ea86a7b9b5541 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -1,68 +1,79 @@ -use std::ops::ControlFlow; - use regex::Regex; +use solar_ast::{ItemFunction, ItemStruct, VariableDefinition}; +use std::ops::ControlFlow; -use solar_ast::{visit::Visit, ItemStruct, VariableDefinition}; - -use super::{ScreamingSnakeCase, StructPascalCase, VariableMixedCase}; - -impl<'ast> Visit<'ast> for VariableMixedCase { - type BreakValue = (); +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{ + FunctionMixedCase, ScreamingSnakeCase, StructPascalCase, VariableMixedCase, + FUNCTION_MIXED_CASE, SCREAMING_SNAKE_CASE, STRUCT_PASCAL_CASE, VARIABLE_MIXED_CASE, + }, +}; - fn visit_variable_definition( +impl<'ast> EarlyLintPass<'ast> for VariableMixedCase { + fn check_variable_definition( &mut self, + ctx: &LintContext<'_>, var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow { + ) -> ControlFlow<()> { if var.mutability.is_none() { if let Some(name) = var.name { let name = name.as_str(); if !is_mixed_case(name) && name.len() > 1 { - self.results.push(var.span); + ctx.emit(&VARIABLE_MIXED_CASE, var.span); } } } - - self.walk_variable_definition(var); ControlFlow::Continue(()) } } -impl<'ast> Visit<'ast> for ScreamingSnakeCase { - type BreakValue = (); +impl<'ast> EarlyLintPass<'ast> for FunctionMixedCase { + fn check_item_function( + &mut self, + ctx: &LintContext<'_>, + func: &'ast ItemFunction<'ast>, + ) -> ControlFlow<()> { + if let Some(name) = func.header.name { + let name = name.as_str(); + if !is_mixed_case(name) && name.len() > 1 { + ctx.emit(&FUNCTION_MIXED_CASE, func.body_span); + } + } + ControlFlow::Continue(()) + } +} - fn visit_variable_definition( +impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { + fn check_variable_definition( &mut self, + ctx: &LintContext<'_>, var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow { + ) -> ControlFlow<()> { if let Some(mutability) = var.mutability { if mutability.is_constant() || mutability.is_immutable() { if let Some(name) = var.name { let name = name.as_str(); if !is_screaming_snake_case(name) && name.len() > 1 { - self.results.push(var.span); + ctx.emit(&SCREAMING_SNAKE_CASE, var.span); } } } } - self.walk_variable_definition(var); ControlFlow::Continue(()) } } -impl<'ast> Visit<'ast> for StructPascalCase { - type BreakValue = (); - - fn visit_item_struct( +impl<'ast> EarlyLintPass<'ast> for StructPascalCase { + fn check_item_struct( &mut self, + ctx: &LintContext<'_>, strukt: &'ast ItemStruct<'ast>, - ) -> ControlFlow { + ) -> ControlFlow<()> { let name = strukt.name.as_str(); - if !is_pascal_case(name) && name.len() > 1 { - self.results.push(strukt.name.span); + ctx.emit(&STRUCT_PASCAL_CASE, strukt.name.span); } - - self.walk_item_struct(strukt); ControlFlow::Continue(()) } } @@ -87,88 +98,78 @@ pub fn is_screaming_snake_case(s: &str) -> bool { #[cfg(test)] mod test { - use solar_ast::{visit::Visit, Arena}; - use solar_interface::{ColorChoice, Session}; use std::path::Path; - use crate::sol::StructPascalCase; - - use super::{ScreamingSnakeCase, VariableMixedCase}; + use super::*; + use crate::{ + linter::Lint, + sol::{SolidityLinter, FUNCTION_MIXED_CASE}, + }; #[test] fn test_variable_mixed_case() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = Arena::new(); - - let mut parser = solar_parse::Parser::from_file( - &sess, - &arena, - Path::new("testdata/VariableMixedCase.sol"), - )?; - - let ast = parser.parse_file().map_err(|e| e.emit())?; + let linter = SolidityLinter::new() + .with_lints(Some(vec![VARIABLE_MIXED_CASE])) + .with_buffer_emitter(true); - let mut pattern = VariableMixedCase::default(); - pattern.visit_source_unit(&ast); + let emitted = + linter.lint_file(Path::new("testdata/VariableMixedCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", VARIABLE_MIXED_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", VARIABLE_MIXED_CASE.id())).count(); - assert_eq!(pattern.results.len(), 6); - - Ok(()) - }); + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 6, "Expected 6 notes"); Ok(()) } #[test] fn test_screaming_snake_case() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = Arena::new(); - - let mut parser = solar_parse::Parser::from_file( - &sess, - &arena, - Path::new("testdata/ScreamingSnakeCase.sol"), - )?; + let linter = SolidityLinter::new() + .with_lints(Some(vec![SCREAMING_SNAKE_CASE])) + .with_buffer_emitter(true); - let ast = parser.parse_file().map_err(|e| e.emit())?; + let emitted = + linter.lint_file(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", SCREAMING_SNAKE_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", SCREAMING_SNAKE_CASE.id())).count(); - let mut pattern = ScreamingSnakeCase::default(); - pattern.visit_source_unit(&ast); - - assert_eq!(pattern.results.len(), 10); - - Ok(()) - }); + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 10, "Expected 10 notes"); Ok(()) } #[test] fn test_struct_pascal_case() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = Arena::new(); - - let mut parser = solar_parse::Parser::from_file( - &sess, - &arena, - Path::new("testdata/StructPascalCase.sol"), - )?; + let linter = SolidityLinter::new() + .with_lints(Some(vec![STRUCT_PASCAL_CASE])) + .with_buffer_emitter(true); - let ast = parser.parse_file().map_err(|e| e.emit())?; + let emitted = + linter.lint_file(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", STRUCT_PASCAL_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", STRUCT_PASCAL_CASE.id())).count(); - let mut pattern = StructPascalCase::default(); - pattern.visit_source_unit(&ast); + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 7, "Expected 7 notes"); - assert_eq!(pattern.results.len(), 5); + Ok(()) + } - Ok(()) - }); + #[test] + fn test_function_mixed_case() -> eyre::Result<()> { + let linter = SolidityLinter::new() + .with_lints(Some(vec![FUNCTION_MIXED_CASE])) + .with_buffer_emitter(true); + + let emitted = + linter.lint_file(Path::new("testdata/FunctionMixedCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", FUNCTION_MIXED_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", FUNCTION_MIXED_CASE.id())).count(); + + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 4, "Expected 4 notes"); Ok(()) } diff --git a/crates/lint/src/sol/med.rs b/crates/lint/src/sol/med.rs index e69975462afe1..75faf9d3dd32b 100644 --- a/crates/lint/src/sol/med.rs +++ b/crates/lint/src/sol/med.rs @@ -1,20 +1,16 @@ use std::ops::ControlFlow; +use solar_ast::{BinOp, BinOpKind, Expr, ExprKind}; -use solar_ast::{visit::Visit, BinOp, BinOpKind, Expr, ExprKind}; +use super::{DivideBeforeMultiply, DIVIDE_BEFORE_MULTIPLY}; +use crate::linter::{EarlyLintPass, LintContext}; -use super::DivideBeforeMultiply; - -impl<'ast> Visit<'ast> for DivideBeforeMultiply { - type BreakValue = (); - - fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow { +impl<'ast> EarlyLintPass<'ast> for DivideBeforeMultiply { + fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) -> ControlFlow<()> { if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { if contains_division(left_expr) { - self.results.push(expr.span); + ctx.emit(&DIVIDE_BEFORE_MULTIPLY, expr.span); } } - - self.walk_expr(expr); ControlFlow::Continue(()) } } @@ -35,35 +31,25 @@ fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { #[cfg(test)] mod test { - use solar_ast::{visit::Visit, Arena}; - use solar_interface::{ColorChoice, Session}; use std::path::Path; - use super::DivideBeforeMultiply; + use super::*; + use crate::{linter::Lint, sol::SolidityLinter}; #[test] fn test_divide_before_multiply() -> eyre::Result<()> { - let sess = Session::builder().with_buffer_emitter(ColorChoice::Auto).build(); - - let _ = sess.enter(|| -> solar_interface::Result<()> { - let arena = Arena::new(); - - let mut parser = solar_parse::Parser::from_file( - &sess, - &arena, - Path::new("testdata/DivideBeforeMultiply.sol"), - )?; - - // Parse the file. - let ast = parser.parse_file().map_err(|e| e.emit())?; - - let mut pattern = DivideBeforeMultiply::default(); - pattern.visit_source_unit(&ast); - - assert_eq!(pattern.results.len(), 6); - - Ok(()) - }); + let linter = SolidityLinter::new() + .with_lints(Some(vec![DIVIDE_BEFORE_MULTIPLY])) + .with_buffer_emitter(true); + + let emitted = + linter.lint_file(Path::new("testdata/DivideBeforeMultiply.sol")).unwrap().to_string(); + let warnings = + emitted.matches(&format!("warning: {}", DIVIDE_BEFORE_MULTIPLY.id())).count(); + let notes = emitted.matches(&format!("note: {}", DIVIDE_BEFORE_MULTIPLY.id())).count(); + + assert_eq!(warnings, 6, "Expected 6 warnings"); + assert_eq!(notes, 0, "Expected 0 notes"); Ok(()) } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index ca80ef6ebb955..dc44e4dfaace2 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -3,69 +3,170 @@ pub mod high; pub mod info; pub mod med; -use std::{ - hash::{Hash, Hasher}, - ops::ControlFlow, - path::PathBuf, -}; - -use crate::linter::{EarlyLintPass, Lint, LintContext, Severity}; +use crate::linter::{EarlyLintPass, EarlyLintVisitor, Lint, LintContext, Linter, Severity}; use foundry_compilers::solc::SolcLanguage; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use solar_ast::{visit::Visit, Arena, SourceUnit}; +use solar_ast::{visit::Visit, Arena}; use solar_interface::{ - diagnostics::{ErrorGuaranteed, Level}, - Session, Span, + diagnostics::{EmittedDiagnostics, ErrorGuaranteed}, + ColorChoice, Session, }; +use std::path::{Path, PathBuf}; use thiserror::Error; -use yansi::Paint; /// Linter implementation to analyze Solidity source code responsible for identifying /// vulnerabilities gas optimizations, and best practices. -pub struct SolidityLinter<'s> { - pub severity: Option>, - pub description: bool, - // Store registered passes. Using Box for dynamic dispatch. - // The lifetime 's links the passes to the Session lifetime. - pub passes: Vec + 's>>, - pub session: &'s Session, +#[derive(Debug, Clone, Default)] +pub struct SolidityLinter { + description: bool, + severity: Option>, + lints_included: Option>, + lints_excluded: Option>, + // This field is only used for testing purposes, in production it will always be false. + with_buffer_emitter: bool, } -impl<'s> SolidityLinter<'s> { - // Pass the session during creation - pub fn new(sess: &'s Session) -> Self { - Self { passes: Vec::new(), sess } +impl SolidityLinter { + pub fn new() -> Self { + Self { + severity: None, + description: false, + lints_included: None, + lints_excluded: None, + with_buffer_emitter: false, + } + } + + pub fn with_severity(mut self, severity: Option>) -> Self { + self.severity = severity; + self + } + + pub fn with_lints(mut self, lints: Option>) -> Self { + self.lints_included = lints; + self + } + + pub fn without_lints(mut self, lints: Option>) -> Self { + self.lints_excluded = lints; + self } - // Method to register passes - pub fn register_early_pass(&mut self, pass: Box + 's>) { - self.passes.push(pass); + #[cfg(test)] + pub(crate) fn with_buffer_emitter(mut self, with: bool) -> Self { + self.with_buffer_emitter = with; + self } - // TODO: Add logic to register passes based on config (e.g., severity from LintArgs) + // Helper function to ease testing, despite `fn lint` being the public API for the `Linter` + pub(crate) fn lint_file(&self, file: &Path) -> Option { + let mut sess = if self.with_buffer_emitter { + Session::builder().with_buffer_emitter(ColorChoice::Never).build() + } else { + Session::builder().with_stderr_emitter().build() + }; + sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); + + let arena = Arena::new(); + + let _ = sess.enter(|| -> Result<(), ErrorGuaranteed> { + // Declare all available passes and lints + let passes_and_lints: Vec<(Box>, SolLint)> = vec![ + (Box::new(AsmKeccak256), ASM_KECCACK256), + (Box::new(IncorrectShift), INCORRECT_SHIFT), + (Box::new(DivideBeforeMultiply), DIVIDE_BEFORE_MULTIPLY), + (Box::new(VariableMixedCase), VARIABLE_MIXED_CASE), + (Box::new(ScreamingSnakeCase), SCREAMING_SNAKE_CASE), + (Box::new(StructPascalCase), STRUCT_PASCAL_CASE), + (Box::new(FunctionMixedCase), FUNCTION_MIXED_CASE), + ]; - // The main method to run the linting for a single AST - pub fn run_passes<'ast>(&mut self, source_unit: &'ast SourceUnit<'ast>) - where - 's: 'ast, // Ensure session lives at least as long as AST - { - // Create the context for this run - let cx = LintContext::new(self.sess); + // Filter based on linter config + let mut passes: Vec>> = passes_and_lints + .into_iter() + .filter_map(|(pass, lint)| { + let matches_severity = match self.severity { + Some(ref target) => target.contains(&lint.severity()), + None => true, + }; + let matches_lints_inc = match self.lints_included { + Some(ref target) => target.contains(&lint), + None => true, + }; + let matches_lints_exc = match self.lints_excluded { + Some(ref target) => target.contains(&lint), + None => false, + }; - // Create a visitor helper struct or implement Visit directly - let mut visitor = LintVisitor { cx: &cx, passes: &mut self.passes }; - visitor.visit_source_unit(source_unit); + if matches_severity && matches_lints_inc && !matches_lints_exc { + Some(pass) + } else { + None + } + }) + .collect(); + + // Initialize the parser and get the AST + let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + let ast = parser.parse_file().map_err(|e| e.emit())?; + + // Initialize and run the visitor + let ctx = LintContext::new(&sess); + let mut visitor = EarlyLintVisitor { ctx: &ctx, passes: &mut passes }; + visitor.visit_source_unit(&ast); + + Ok(()) + }); + + sess.emitted_diagnostics() + } +} + +impl Linter for SolidityLinter { + type Language = SolcLanguage; + type Lint = SolLint; + + fn lint(&self, input: &[PathBuf]) { + input.into_par_iter().for_each(|file| { + _ = self.lint_file(file); + }); } } #[derive(Error, Debug)] -pub enum SolLintError {} +pub enum SolLintError { + #[error("Unknown lint ID: {0}")] + InvalidId(String) +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct SolLint { + id: &'static str, + description: &'static str, + help: Option<&'static str>, + severity: Severity, +} + +impl Lint for SolLint { + fn id(&self) -> &'static str { + self.id + } + fn severity(&self) -> Severity { + self.severity + } + fn description(&self) -> &'static str { + self.description + } + fn help(&self) -> Option<&'static str> { + self.help + } +} macro_rules! declare_forge_lints { - ($(($struct_id:ident, $lint_id:ident, $severity:expr, $str_id:expr, $description:expr, $help:expr)),* $(,)?) => { + ($(($pass_id:ident, $lint_id:ident, $severity:expr, $str_id:expr, $description:expr, $help:expr)),* $(,)?) => { // Declare the static `Lint` metadata $( - pub static $lint_id: crate::linter::Lint = crate::linter::Lint { + pub static $lint_id: SolLint = SolLint { id: $str_id, severity: $severity, description: $description, @@ -73,31 +174,43 @@ macro_rules! declare_forge_lints { }; )* - // Declare the structs that will implement the pass trait - $( - #[derive(Debug, Default, Clone, Copy)] - pub struct $struct_id; + // Implement TryFrom<&str> for SolLint + impl<'a> TryFrom<&'a str> for SolLint { + type Error = SolLintError; - impl $struct_id { - pub fn new() -> Self { Self } + fn try_from(value: &'a str) -> Result { + match value { + $( + // Match the input string against the static string ID + $str_id => Ok($lint_id), // Return the corresponding static SolLint instance + )* + // If no match is found, return an error + _ => Err(SolLintError::InvalidId(value.to_string())), + } } + } + + // Declare the structs that will implement the pass trait + $( + #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] + pub struct $pass_id; )* }; } -/// Macro for defining lints and relevant metadata for the Solidity linter. -/// -/// This macro generates the [`SolLint`] enum with each lint along with utility methods and -/// corresponding structs for each lint specified. -/// -/// # Parameters -/// -/// Each lint is defined as a tuple with the following fields: -/// - `$id`: Identitifier used as the struct and enum variant created for the lint. -/// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). -/// - `$description`: A short description of the lint. -/// - `$help`: Link to additional information about the lint or best practices. -/// - `$str_id`: A unique identifier used to reference a specific lint during configuration. +// Macro for defining lints and relevant metadata for the Solidity linter. +// +// This macro generates the [`SolLint`] enum with each lint along with utility methods and +// corresponding structs for each lint specified. +// +// # Parameters +// +// Each lint is defined as a tuple with the following fields: +// - `$id`: Identitifier used as the struct and enum variant created for the lint. +// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). +// - `$description`: A short description of the lint. +// - `$help`: Link to additional information about the lint or best practices. +// - `$str_id`: A unique identifier used to reference a specific lint during configuration. declare_forge_lints!( //High ( @@ -144,10 +257,23 @@ declare_forge_lints!( "Structs should use PascalCase.", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names" ), - // TODO: FunctionMixedCase - + ( + FunctionMixedCase, + FUNCTION_MIXED_CASE, + Severity::Info, + "function-mixed-case", + "Function names should use mixedCase.", + "https://docs.soliditylang.org/en/latest/style-guide.html#function-names" + ), // Gas Optimizations - (AsmKeccak256, ASM_KECCACK256, Severity::Gas, "asm-keccack256", "Hash via inline assembly to save gas", ""), + ( + AsmKeccak256, + ASM_KECCACK256, + Severity::Gas, + "asm-keccack256", + "Hash via inline assembly to save gas", + "" + ), // TODO: PackStorageVariables // TODO: PackStructs // TODO: UseConstantVariable diff --git a/crates/lint/testdata/FunctionMixedCase.sol b/crates/lint/testdata/FunctionMixedCase.sol new file mode 100644 index 0000000000000..af807c12f09c1 --- /dev/null +++ b/crates/lint/testdata/FunctionMixedCase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract FunctionMixedCaseTest { + // Passes + function functionMixedCase() public {} + function _functionMixedCase() internal {} + + // Fails + function Functionmixedcase() public {} + function FUNCTION_MIXED_CASE() public {} + function functionmixedcase() public {} + function FunctionMixedCase() public {} +} diff --git a/crates/lint/testdata/IncorrectShift.sol b/crates/lint/testdata/IncorrectShift.sol new file mode 100644 index 0000000000000..6dc1edb2b46a0 --- /dev/null +++ b/crates/lint/testdata/IncorrectShift.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract IncorrectShift { + uint256 stateValue = 100; + uint256 stateShiftAmount = 4; + + function getAmount() public view returns (uint256) { + return stateShiftAmount; + } + + function shift() public view { + uint256 result; + uint256 localValue = 50; + uint256 localShiftAmount = 3; + + // SHOULD FAIL: + // - Literal << NonLiteral + // - Literal >> NonLiteral + + result = 2 << stateValue; + result = 8 >> localValue; + result = 16 << (stateValue + 1); + result = 32 >> getAmount(); + result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); + + // SHOULD PASS: + result = stateValue << 2; + result = localValue >> 3; + result = stateValue << localShiftAmount; + result = localValue >> stateShiftAmount; + result = (stateValue * 2) << 4; + result = getAmount() >> 1; + + result = 1 << 8; + result = 255 >> 4; + } +} From ab8a5de69095d22cf36bbfc6ff99c1c8348b5dff Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 29 Apr 2025 01:27:24 +0200 Subject: [PATCH 073/107] fix: fmt + clippy --- crates/forge/src/cmd/lint.rs | 2 +- crates/lint/src/linter.rs | 16 +++++++++++----- crates/lint/src/sol/gas.rs | 20 +++++++++----------- crates/lint/src/sol/high.rs | 25 ++++++++++++++++--------- crates/lint/src/sol/med.rs | 2 +- crates/lint/src/sol/mod.rs | 19 +++++++++++-------- 6 files changed, 49 insertions(+), 35 deletions(-) diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index 216d7756ed1a8..b425dde0f3302 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -79,7 +79,7 @@ impl LintArgs { // Resolve and apply included lints if provided if let Some(ref include_ids) = self.include_lint { - let included_lints = include_ids + let included_lints = include_ids .iter() .map(|id_str| SolLint::try_from(id_str.as_str())) .collect::, SolLintError>>()?; diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index 127fbeb792c2a..c693f034a31b2 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -81,17 +81,23 @@ impl fmt::Display for Severity { } pub struct LintContext<'s> { - pub sess: &'s Session, + sess: &'s Session, + desc: bool, } impl<'s> LintContext<'s> { - pub fn new(sess: &'s Session) -> Self { - Self { sess } + pub fn new(sess: &'s Session, with_description: bool) -> Self { + Self { sess, desc: with_description } } // Helper method to emit diagnostics easily from passes pub fn emit(&self, lint: &'static L, span: Span) { - let msg = format!("{}: {}", lint.id(), lint.description()); + let msg = if self.desc { + format!("{}: {}", lint.id(), lint.description()) + } else { + lint.id().into() + }; + let diag: DiagBuilder<'_, ()> = match lint.help() { Some(help) => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(help), None => self.sess.dcx.diag(lint.severity().into(), msg).span(span), @@ -138,7 +144,7 @@ pub struct EarlyLintVisitor<'a, 's, 'ast> { pub passes: &'a mut [Box + 's>], } -impl<'a, 's, 'ast> Visit<'ast> for EarlyLintVisitor<'a, 's, 'ast> +impl<'s, 'ast> Visit<'ast> for EarlyLintVisitor<'_, 's, 'ast> where 's: 'ast, { diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 43c63e86b206f..855effa9fcba0 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -1,15 +1,15 @@ -use std::ops::ControlFlow; use solar_ast::{Expr, ExprKind}; +use std::ops::ControlFlow; -use crate::linter::EarlyLintPass; use super::{AsmKeccak256, ASM_KECCACK256}; +use crate::linter::EarlyLintPass; impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { fn check_expr( - &mut self, - ctx: &crate::linter::LintContext<'_>, - expr: &'ast Expr<'ast>, - ) -> ControlFlow<()> { + &mut self, + ctx: &crate::linter::LintContext<'_>, + expr: &'ast Expr<'ast>, + ) -> ControlFlow<()> { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { if ident.name.as_str() == "keccak256" { @@ -30,12 +30,10 @@ mod test { #[test] fn test_keccak256() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![ASM_KECCACK256])) - .with_buffer_emitter(true); + let linter = + SolidityLinter::new().with_lints(Some(vec![ASM_KECCACK256])).with_buffer_emitter(true); - let emitted = - linter.lint_file(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); + let emitted = linter.lint_file(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning: {}", ASM_KECCACK256.id())).count(); let notes = emitted.matches(&format!("note: {}", ASM_KECCACK256.id())).count(); diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high.rs index a9535064be42a..116528ba19b73 100644 --- a/crates/lint/src/sol/high.rs +++ b/crates/lint/src/sol/high.rs @@ -1,11 +1,16 @@ -use std::ops::ControlFlow; use solar_ast::{BinOp, BinOpKind, Expr, ExprKind}; +use std::ops::ControlFlow; use super::{EarlyLintPass, IncorrectShift, LintContext, INCORRECT_SHIFT}; impl<'ast> EarlyLintPass<'ast> for IncorrectShift { fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) -> ControlFlow<()> { - if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Shl | BinOpKind::Shr, .. }, right_expr) = &expr.kind { + if let ExprKind::Binary( + left_expr, + BinOp { kind: BinOpKind::Shl | BinOpKind::Shr, .. }, + right_expr, + ) = &expr.kind + { if contains_incorrect_shift(left_expr, right_expr) { ctx.emit(&INCORRECT_SHIFT, expr.span); } @@ -15,8 +20,12 @@ impl<'ast> EarlyLintPass<'ast> for IncorrectShift { } // TODO: come up with a better heuristic. Treat initial impl as a PoC. -// Checks if the left operand is a literal and the right operand is not, indicating a potential reversed shift operation. -fn contains_incorrect_shift<'ast>(left_expr: &'ast Expr<'ast>, right_expr: &'ast Expr<'ast>) -> bool { +// Checks if the left operand is a literal and the right operand is not, indicating a potential +// reversed shift operation. +fn contains_incorrect_shift<'ast>( + left_expr: &'ast Expr<'ast>, + right_expr: &'ast Expr<'ast>, +) -> bool { let is_left_literal = matches!(left_expr.kind, ExprKind::Lit(..)); let is_right_not_literal = !matches!(right_expr.kind, ExprKind::Lit(..)); @@ -32,14 +41,12 @@ mod test { #[test] fn test_incorrect_shift() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![INCORRECT_SHIFT])) - .with_buffer_emitter(true); + let linter = + SolidityLinter::new().with_lints(Some(vec![INCORRECT_SHIFT])).with_buffer_emitter(true); let emitted = linter.lint_file(Path::new("testdata/IncorrectShift.sol")).unwrap().to_string(); - let warnings = - emitted.matches(&format!("warning: {}", INCORRECT_SHIFT.id())).count(); + let warnings = emitted.matches(&format!("warning: {}", INCORRECT_SHIFT.id())).count(); let notes = emitted.matches(&format!("note: {}", INCORRECT_SHIFT.id())).count(); assert_eq!(warnings, 5, "Expected 5 warnings"); diff --git a/crates/lint/src/sol/med.rs b/crates/lint/src/sol/med.rs index 75faf9d3dd32b..30bb97059e13a 100644 --- a/crates/lint/src/sol/med.rs +++ b/crates/lint/src/sol/med.rs @@ -1,5 +1,5 @@ -use std::ops::ControlFlow; use solar_ast::{BinOp, BinOpKind, Expr, ExprKind}; +use std::ops::ControlFlow; use super::{DivideBeforeMultiply, DIVIDE_BEFORE_MULTIPLY}; use crate::linter::{EarlyLintPass, LintContext}; diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index dc44e4dfaace2..78856aed44605 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -18,10 +18,10 @@ use thiserror::Error; /// vulnerabilities gas optimizations, and best practices. #[derive(Debug, Clone, Default)] pub struct SolidityLinter { - description: bool, severity: Option>, lints_included: Option>, lints_excluded: Option>, + with_description: bool, // This field is only used for testing purposes, in production it will always be false. with_buffer_emitter: bool, } @@ -30,9 +30,9 @@ impl SolidityLinter { pub fn new() -> Self { Self { severity: None, - description: false, lints_included: None, lints_excluded: None, + with_description: false, with_buffer_emitter: false, } } @@ -52,6 +52,11 @@ impl SolidityLinter { self } + pub fn with_description(mut self, description: bool) -> Self { + self.with_description = description; + self + } + #[cfg(test)] pub(crate) fn with_buffer_emitter(mut self, with: bool) -> Self { self.with_buffer_emitter = with; @@ -111,7 +116,7 @@ impl SolidityLinter { let ast = parser.parse_file().map_err(|e| e.emit())?; // Initialize and run the visitor - let ctx = LintContext::new(&sess); + let ctx = LintContext::new(&sess, self.with_description); let mut visitor = EarlyLintVisitor { ctx: &ctx, passes: &mut passes }; visitor.visit_source_unit(&ast); @@ -136,7 +141,7 @@ impl Linter for SolidityLinter { #[derive(Error, Debug)] pub enum SolLintError { #[error("Unknown lint ID: {0}")] - InvalidId(String) + InvalidId(String), } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -174,17 +179,15 @@ macro_rules! declare_forge_lints { }; )* - // Implement TryFrom<&str> for SolLint + // Implement TryFrom<&str> for `SolLint` impl<'a> TryFrom<&'a str> for SolLint { type Error = SolLintError; fn try_from(value: &'a str) -> Result { match value { $( - // Match the input string against the static string ID - $str_id => Ok($lint_id), // Return the corresponding static SolLint instance + $str_id => Ok($lint_id), )* - // If no match is found, return an error _ => Err(SolLintError::InvalidId(value.to_string())), } } From 9e63c1965c2170874be0dcc78e60bcad38b72479 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 29 Apr 2025 08:02:19 +0200 Subject: [PATCH 074/107] fix: fmt + clippy --- crates/anvil/core/src/eth/mod.rs | 1 - crates/anvil/core/src/types.rs | 1 - crates/cast/src/tx.rs | 1 - crates/doc/src/document.rs | 1 - crates/doc/src/parser/item.rs | 1 - crates/evm/evm/src/executors/fuzz/types.rs | 1 - crates/evm/fuzz/src/lib.rs | 1 - crates/lint/src/sol/mod.rs | 4 ++-- 8 files changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index d380c3cc47e22..b28913c7067c2 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -37,7 +37,6 @@ pub struct Params { /// Represents ethereum JSON-RPC API #[derive(Clone, Debug, serde::Deserialize)] #[serde(tag = "method", content = "params")] -#[expect(clippy::large_enum_variant)] pub enum EthRequest { #[serde(rename = "web3_clientVersion", with = "empty_params")] Web3ClientVersion(()), diff --git a/crates/anvil/core/src/types.rs b/crates/anvil/core/src/types.rs index e9d9c682e12dc..e90275a37aaea 100644 --- a/crates/anvil/core/src/types.rs +++ b/crates/anvil/core/src/types.rs @@ -37,7 +37,6 @@ pub struct ReorgOptions { #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] -#[expect(clippy::large_enum_variant)] pub enum TransactionData { JSON(TransactionRequest), Raw(Bytes), diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 1097da794e15f..caf47e28440e0 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -25,7 +25,6 @@ use serde_json::value::RawValue; use std::fmt::Write; /// Different sender kinds used by [`CastTxBuilder`]. -#[expect(clippy::large_enum_variant)] pub enum SenderKind<'a> { /// An address without signer. Used for read-only calls and transactions sent through unlocked /// accounts. diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 63b81e020083f..10f72a672c256 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -80,7 +80,6 @@ impl Document { /// The content of the document. #[derive(Debug)] -#[allow(clippy::large_enum_variant)] pub enum DocumentContent { Empty, Single(ParseItem), diff --git a/crates/doc/src/parser/item.rs b/crates/doc/src/parser/item.rs index 5cc39f3e9aec6..999758cebc992 100644 --- a/crates/doc/src/parser/item.rs +++ b/crates/doc/src/parser/item.rs @@ -148,7 +148,6 @@ impl ParseItem { /// A wrapper type around pt token. #[derive(Clone, Debug, PartialEq, Eq)] -#[allow(clippy::large_enum_variant)] pub enum ParseSource { /// Source contract definition. Contract(Box), diff --git a/crates/evm/evm/src/executors/fuzz/types.rs b/crates/evm/evm/src/executors/fuzz/types.rs index 1bda778e7c9cf..f5399c5c3ac1a 100644 --- a/crates/evm/evm/src/executors/fuzz/types.rs +++ b/crates/evm/evm/src/executors/fuzz/types.rs @@ -36,7 +36,6 @@ pub struct CounterExampleOutcome { /// Outcome of a single fuzz #[derive(Debug)] -#[expect(clippy::large_enum_variant)] pub enum FuzzOutcome { Case(CaseOutcome), CounterExample(CounterExampleOutcome), diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 3486dac0b5172..65ef76f16c989 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -32,7 +32,6 @@ mod inspector; pub use inspector::Fuzzer; #[derive(Clone, Debug, Serialize, Deserialize)] -#[expect(clippy::large_enum_variant)] pub enum CounterExample { /// Call used as a counter example for fuzz tests. Single(BaseCounterExample), diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 78856aed44605..675783094284c 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -52,8 +52,8 @@ impl SolidityLinter { self } - pub fn with_description(mut self, description: bool) -> Self { - self.with_description = description; + pub fn with_description(mut self, with: bool) -> Self { + self.with_description = with; self } From 90254269c2880815799cbbb98450583165d0e727 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 29 Apr 2025 14:24:45 +0200 Subject: [PATCH 075/107] fix: fmt + clippy --- crates/lint/src/sol/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 675783094284c..c0c498a41ae8f 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -118,7 +118,7 @@ impl SolidityLinter { // Initialize and run the visitor let ctx = LintContext::new(&sess, self.with_description); let mut visitor = EarlyLintVisitor { ctx: &ctx, passes: &mut passes }; - visitor.visit_source_unit(&ast); + let _ = visitor.visit_source_unit(&ast); Ok(()) }); From 3b2c81fa7c416fdaf49f4b30bd8c8680c80c5dc3 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 29 Apr 2025 15:10:07 +0200 Subject: [PATCH 076/107] fix: fmt + clippy --- crates/anvil/core/src/eth/mod.rs | 1 + crates/anvil/core/src/types.rs | 1 + crates/cast/src/tx.rs | 1 + crates/doc/src/document.rs | 1 + crates/doc/src/parser/item.rs | 1 + crates/evm/evm/src/executors/fuzz/types.rs | 1 + crates/evm/fuzz/src/lib.rs | 1 + 7 files changed, 7 insertions(+) diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index b28913c7067c2..d380c3cc47e22 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -37,6 +37,7 @@ pub struct Params { /// Represents ethereum JSON-RPC API #[derive(Clone, Debug, serde::Deserialize)] #[serde(tag = "method", content = "params")] +#[expect(clippy::large_enum_variant)] pub enum EthRequest { #[serde(rename = "web3_clientVersion", with = "empty_params")] Web3ClientVersion(()), diff --git a/crates/anvil/core/src/types.rs b/crates/anvil/core/src/types.rs index e90275a37aaea..e9d9c682e12dc 100644 --- a/crates/anvil/core/src/types.rs +++ b/crates/anvil/core/src/types.rs @@ -37,6 +37,7 @@ pub struct ReorgOptions { #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] +#[expect(clippy::large_enum_variant)] pub enum TransactionData { JSON(TransactionRequest), Raw(Bytes), diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index caf47e28440e0..1097da794e15f 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -25,6 +25,7 @@ use serde_json::value::RawValue; use std::fmt::Write; /// Different sender kinds used by [`CastTxBuilder`]. +#[expect(clippy::large_enum_variant)] pub enum SenderKind<'a> { /// An address without signer. Used for read-only calls and transactions sent through unlocked /// accounts. diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 10f72a672c256..987603b3b6993 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -80,6 +80,7 @@ impl Document { /// The content of the document. #[derive(Debug)] +#[expect(clippy::large_enum_variant)] pub enum DocumentContent { Empty, Single(ParseItem), diff --git a/crates/doc/src/parser/item.rs b/crates/doc/src/parser/item.rs index 999758cebc992..300d2703dbd65 100644 --- a/crates/doc/src/parser/item.rs +++ b/crates/doc/src/parser/item.rs @@ -148,6 +148,7 @@ impl ParseItem { /// A wrapper type around pt token. #[derive(Clone, Debug, PartialEq, Eq)] +#[expect(clippy::large_enum_variant)] pub enum ParseSource { /// Source contract definition. Contract(Box), diff --git a/crates/evm/evm/src/executors/fuzz/types.rs b/crates/evm/evm/src/executors/fuzz/types.rs index f5399c5c3ac1a..1bda778e7c9cf 100644 --- a/crates/evm/evm/src/executors/fuzz/types.rs +++ b/crates/evm/evm/src/executors/fuzz/types.rs @@ -36,6 +36,7 @@ pub struct CounterExampleOutcome { /// Outcome of a single fuzz #[derive(Debug)] +#[expect(clippy::large_enum_variant)] pub enum FuzzOutcome { Case(CaseOutcome), CounterExample(CounterExampleOutcome), diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 65ef76f16c989..3486dac0b5172 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -32,6 +32,7 @@ mod inspector; pub use inspector::Fuzzer; #[derive(Clone, Debug, Serialize, Deserialize)] +#[expect(clippy::large_enum_variant)] pub enum CounterExample { /// Call used as a counter example for fuzz tests. Single(BaseCounterExample), From 5042c881c566a08e1538fd725913997c68e9f9c5 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Wed, 30 Apr 2025 23:20:05 +0200 Subject: [PATCH 077/107] fix: feedback --- crates/doc/src/document.rs | 2 +- crates/doc/src/parser/item.rs | 2 +- crates/lint/src/linter.rs | 6 +- crates/lint/src/sol/gas.rs | 3 +- crates/lint/src/sol/info.rs | 69 +++++++++++++-------- crates/lint/testdata/FunctionMixedCase.sol | 14 ----- crates/lint/testdata/ScreamingSnakeCase.sol | 2 +- crates/lint/testdata/VariableMixedCase.sol | 25 -------- 8 files changed, 51 insertions(+), 72 deletions(-) delete mode 100644 crates/lint/testdata/FunctionMixedCase.sol delete mode 100644 crates/lint/testdata/VariableMixedCase.sol diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 987603b3b6993..63b81e020083f 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -80,7 +80,7 @@ impl Document { /// The content of the document. #[derive(Debug)] -#[expect(clippy::large_enum_variant)] +#[allow(clippy::large_enum_variant)] pub enum DocumentContent { Empty, Single(ParseItem), diff --git a/crates/doc/src/parser/item.rs b/crates/doc/src/parser/item.rs index 300d2703dbd65..5cc39f3e9aec6 100644 --- a/crates/doc/src/parser/item.rs +++ b/crates/doc/src/parser/item.rs @@ -148,7 +148,7 @@ impl ParseItem { /// A wrapper type around pt token. #[derive(Clone, Debug, PartialEq, Eq)] -#[expect(clippy::large_enum_variant)] +#[allow(clippy::large_enum_variant)] pub enum ParseSource { /// Source contract definition. Contract(Box), diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index c693f034a31b2..d75a4572193bc 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -92,15 +92,15 @@ impl<'s> LintContext<'s> { // Helper method to emit diagnostics easily from passes pub fn emit(&self, lint: &'static L, span: Span) { - let msg = if self.desc { - format!("{}: {}", lint.id(), lint.description()) + let msg = if self.desc && lint.help().is_some() { + format!("{}\n --> {}", lint.id(), lint.description()) } else { lint.id().into() }; let diag: DiagBuilder<'_, ()> = match lint.help() { Some(help) => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(help), - None => self.sess.dcx.diag(lint.severity().into(), msg).span(span), + None => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(lint.description()), }; diag.emit(); diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas.rs index 855effa9fcba0..38dc6b113f16b 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas.rs @@ -1,4 +1,5 @@ use solar_ast::{Expr, ExprKind}; +use solar_interface::kw::Keccak256; use std::ops::ControlFlow; use super::{AsmKeccak256, ASM_KECCACK256}; @@ -12,7 +13,7 @@ impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { ) -> ControlFlow<()> { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { - if ident.name.as_str() == "keccak256" { + if ident.name == Keccak256 { ctx.emit(&ASM_KECCACK256, expr.span); } } diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index ea86a7b9b5541..3640d97d6ed6d 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -19,7 +19,7 @@ impl<'ast> EarlyLintPass<'ast> for VariableMixedCase { if var.mutability.is_none() { if let Some(name) = var.name { let name = name.as_str(); - if !is_mixed_case(name) && name.len() > 1 { + if !is_mixed_case(name) { ctx.emit(&VARIABLE_MIXED_CASE, var.span); } } @@ -78,22 +78,39 @@ impl<'ast> EarlyLintPass<'ast> for StructPascalCase { } } -// Check if a string is mixedCase +/// Check if a string is mixedCase +/// +/// To avoid false positives like `fn increment()` or `uin256 counter`, +/// lowercase strings are treated as mixedCase. pub fn is_mixed_case(s: &str) -> bool { + if s.len() <= 1 { + return true; + } + let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); - re.is_match(s) && s.chars().any(|c| c.is_uppercase()) + re.is_match(s) } -// Check if a string is PascalCase +/// Check if a string is PascalCase pub fn is_pascal_case(s: &str) -> bool { + if s.len() <= 1 { + return true; + } + let re = Regex::new(r"^[A-Z][a-z]+(?:[A-Z][a-z]+)*$").unwrap(); re.is_match(s) } -// Check if a string is SCREAMING_SNAKE_CASE +/// Check if a string is SCREAMING_SNAKE_CASE, where +/// numbers must always be preceeded by an underscode. pub fn is_screaming_snake_case(s: &str) -> bool { + if s.len() <= 1 { + return true; + } + let re = Regex::new(r"^[A-Z_][A-Z0-9_]*$").unwrap(); - re.is_match(s) && s.contains('_') + let invalid_re = Regex::new(r"[A-Z][0-9]").unwrap(); + re.is_match(s) && !invalid_re.is_match(s) } #[cfg(test)] @@ -113,63 +130,63 @@ mod test { .with_buffer_emitter(true); let emitted = - linter.lint_file(Path::new("testdata/VariableMixedCase.sol")).unwrap().to_string(); + linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning: {}", VARIABLE_MIXED_CASE.id())).count(); let notes = emitted.matches(&format!("note: {}", VARIABLE_MIXED_CASE.id())).count(); assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 6, "Expected 6 notes"); + assert_eq!(notes, 5, "Expected 5 notes"); Ok(()) } #[test] - fn test_screaming_snake_case() -> eyre::Result<()> { + fn test_function_mixed_case() -> eyre::Result<()> { let linter = SolidityLinter::new() - .with_lints(Some(vec![SCREAMING_SNAKE_CASE])) + .with_lints(Some(vec![FUNCTION_MIXED_CASE])) .with_buffer_emitter(true); let emitted = - linter.lint_file(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", SCREAMING_SNAKE_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", SCREAMING_SNAKE_CASE.id())).count(); + linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", FUNCTION_MIXED_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", FUNCTION_MIXED_CASE.id())).count(); assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 10, "Expected 10 notes"); + assert_eq!(notes, 3, "Expected 3 notes"); Ok(()) } #[test] - fn test_struct_pascal_case() -> eyre::Result<()> { + fn test_screaming_snake_case() -> eyre::Result<()> { let linter = SolidityLinter::new() - .with_lints(Some(vec![STRUCT_PASCAL_CASE])) + .with_lints(Some(vec![SCREAMING_SNAKE_CASE])) .with_buffer_emitter(true); let emitted = - linter.lint_file(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", STRUCT_PASCAL_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", STRUCT_PASCAL_CASE.id())).count(); + linter.lint_file(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", SCREAMING_SNAKE_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", SCREAMING_SNAKE_CASE.id())).count(); assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 7, "Expected 7 notes"); + assert_eq!(notes, 9, "Expected 9 notes"); Ok(()) } #[test] - fn test_function_mixed_case() -> eyre::Result<()> { + fn test_struct_pascal_case() -> eyre::Result<()> { let linter = SolidityLinter::new() - .with_lints(Some(vec![FUNCTION_MIXED_CASE])) + .with_lints(Some(vec![STRUCT_PASCAL_CASE])) .with_buffer_emitter(true); let emitted = - linter.lint_file(Path::new("testdata/FunctionMixedCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", FUNCTION_MIXED_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", FUNCTION_MIXED_CASE.id())).count(); + linter.lint_file(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning: {}", STRUCT_PASCAL_CASE.id())).count(); + let notes = emitted.matches(&format!("note: {}", STRUCT_PASCAL_CASE.id())).count(); assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 4, "Expected 4 notes"); + assert_eq!(notes, 7, "Expected 7 notes"); Ok(()) } diff --git a/crates/lint/testdata/FunctionMixedCase.sol b/crates/lint/testdata/FunctionMixedCase.sol deleted file mode 100644 index af807c12f09c1..0000000000000 --- a/crates/lint/testdata/FunctionMixedCase.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -contract FunctionMixedCaseTest { - // Passes - function functionMixedCase() public {} - function _functionMixedCase() internal {} - - // Fails - function Functionmixedcase() public {} - function FUNCTION_MIXED_CASE() public {} - function functionmixedcase() public {} - function FunctionMixedCase() public {} -} diff --git a/crates/lint/testdata/ScreamingSnakeCase.sol b/crates/lint/testdata/ScreamingSnakeCase.sol index ca4d72fb6d06d..a3cb29532d154 100644 --- a/crates/lint/testdata/ScreamingSnakeCase.sol +++ b/crates/lint/testdata/ScreamingSnakeCase.sol @@ -7,9 +7,9 @@ contract ScreamingSnakeCaseTest { uint256 constant SCREAMING_SNAKE_CASE = 0; uint256 immutable _SCREAMING_SNAKE_CASE_1 = 0; uint256 immutable SCREAMING_SNAKE_CASE_1 = 0; + uint256 constant SCREAMINGSNAKECASE = 0; // Fails - uint256 constant SCREAMINGSNAKECASE = 0; uint256 constant screamingSnakeCase = 0; uint256 constant screaming_snake_case = 0; uint256 constant ScreamingSnakeCase = 0; diff --git a/crates/lint/testdata/VariableMixedCase.sol b/crates/lint/testdata/VariableMixedCase.sol deleted file mode 100644 index 77f28b226196b..0000000000000 --- a/crates/lint/testdata/VariableMixedCase.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -contract VariableMixedCaseTest { - // Passes - uint256 variableMixedCase; - uint256 _variableMixedCase; - - // Fails - uint256 Variablemixedcase; - uint256 VARIABLE_MIXED_CASE; - uint256 variablemixedcase; - uint256 VariableMixedCase; - - function foo() public { - // Passes - uint256 testVal; - uint256 testVAL; - uint256 testVal123; - - // Fails - uint256 TestVal; - uint256 TESTVAL; - } -} From 2c9803297f84a8fac5f2539989f76d9f92678746 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Wed, 30 Apr 2025 23:20:47 +0200 Subject: [PATCH 078/107] fix: feedback --- crates/lint/testdata/MixedCase.sol | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 crates/lint/testdata/MixedCase.sol diff --git a/crates/lint/testdata/MixedCase.sol b/crates/lint/testdata/MixedCase.sol new file mode 100644 index 0000000000000..ebe71c159fce4 --- /dev/null +++ b/crates/lint/testdata/MixedCase.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MixedCaseTest { + // Passes + uint256 variableMixedCase; + uint256 _variableMixedCase; + uint256 variablemixedcase; + + // Fails + uint256 Variablemixedcase; + uint256 VARIABLE_MIXED_CASE; + uint256 VariableMixedCase; + + function foo() public { + // Passes + uint256 testVal; + uint256 testVAL; + uint256 testVal123; + + // Fails + uint256 TestVal; + uint256 TESTVAL; + } + + // Passes + function functionMixedCase() public {} + function _functionMixedCase() internal {} + function functionmixedcase() public {} + + // Fails + function Functionmixedcase() public {} + function FUNCTION_MIXED_CASE() public {} + function FunctionMixedCase() public {} +} From c12aa46cb5a761a335281dfc70685ea60c26978b Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Thu, 1 May 2025 17:15:32 +0200 Subject: [PATCH 079/107] fix: regex Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> --- crates/lint/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 93458cebabab2..205dea949c9ce 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -29,5 +29,5 @@ serde_json.workspace = true auto_impl.workspace = true yansi.workspace = true serde = { workspace = true, features = ["derive"] } -regex = "1.11" +regex.workspace = true clap = { version = "4", features = ["derive"] } From 63cec9269eb57f17512727e8aef4371f7919a948 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Fri, 2 May 2025 18:41:03 +0200 Subject: [PATCH 080/107] tests: cli integration --- Cargo.lock | 4 +- crates/config/Cargo.toml | 2 + crates/config/src/lib.rs | 33 +++++++ crates/forge/src/cmd/lint.rs | 146 ++++++++++++++++++------------- crates/forge/tests/cli/cmd.rs | 8 +- crates/forge/tests/cli/config.rs | 13 +++ crates/forge/tests/cli/main.rs | 1 + crates/lint/Cargo.toml | 4 +- crates/lint/src/linter.rs | 56 ++---------- crates/lint/src/sol/info.rs | 10 +-- crates/lint/src/sol/mod.rs | 3 +- 11 files changed, 155 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 043c0cb320568..c5f78dd33e924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3585,10 +3585,10 @@ name = "forge-lint" version = "1.1.0" dependencies = [ "auto_impl", - "clap", "eyre", "foundry-common", "foundry-compilers", + "foundry-config", "rayon", "regex", "serde", @@ -4025,6 +4025,7 @@ version = "1.1.0" dependencies = [ "alloy-chains", "alloy-primitives", + "clap", "dirs", "dunce", "eyre", @@ -4045,6 +4046,7 @@ dependencies = [ "serde", "serde_json", "similar-asserts", + "solar-interface", "solar-parse", "soldeer-core", "tempfile", diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 816f06054734f..d5897fd89ad4a 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -22,6 +22,7 @@ alloy-primitives = { workspace = true, features = ["serde"] } revm-primitives.workspace = true solar-parse.workspace = true +solar-interface.workspace = true dirs.workspace = true dunce.workspace = true @@ -45,6 +46,7 @@ toml_edit = "0.22" tracing.workspace = true walkdir.workspace = true yansi.workspace = true +clap = { version = "4", features = ["derive"] } [target.'cfg(target_os = "windows")'.dependencies] path-slash = "0.2" diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b7d3f0f7e5c0d..612319f15a99a 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -74,6 +74,9 @@ use cache::{Cache, ChainCache}; pub mod fmt; pub use fmt::FormatterConfig; +pub mod lint; +pub use lint::{LinterConfig, Severity as LintSeverity}; + pub mod fs_permissions; pub use fs_permissions::FsPermissions; use fs_permissions::PathPermission; @@ -446,6 +449,8 @@ pub struct Config { pub build_info_path: Option, /// Configuration for `forge fmt` pub fmt: FormatterConfig, + /// Configuration for `forge lint` + pub lint: LinterConfig, /// Configuration for `forge doc` pub doc: DocConfig, /// Configuration for `forge bind-json` @@ -559,6 +564,7 @@ impl Config { "rpc_endpoints", "etherscan", "fmt", + "lint", "doc", "fuzz", "invariant", @@ -2393,6 +2399,7 @@ impl Default for Config { build_info: false, build_info_path: None, fmt: Default::default(), + lint: Default::default(), doc: Default::default(), bind_json: Default::default(), labels: Default::default(), @@ -4433,6 +4440,32 @@ mod tests { }); } + #[test] + fn test_lint_config() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r" + [lint] + severity = ['high', 'medium'] + include_lints = ['function-mixed-case'] + exclude_lints = ['incorrect-shift'] + ", + )?; + let loaded = Config::load().unwrap().sanitized(); + assert_eq!( + loaded.lint, + LinterConfig { + severity: vec![LintSeverity::High, LintSeverity::Med], + exclude_lints: vec!["incorrect-shift".into()], + ..Default::default() + } + ); + + Ok(()) + }); + } + #[test] fn test_invariant_config() { figment::Jail::expect_with(|jail| { diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index b425dde0f3302..292e803b5dfa0 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -1,16 +1,21 @@ use clap::{Parser, ValueHint}; use eyre::Result; use forge_lint::{ - linter::{Linter, Severity}, + linter::Linter, sol::{SolLint, SolLintError, SolidityLinter}, }; -use foundry_cli::utils::LoadConfig; -use foundry_config::impl_figment_convert_basic; +use foundry_cli::utils::{FoundryPathExt, LoadConfig}; +use foundry_compilers::{solc::SolcLanguage, utils::SOLC_EXTENSIONS}; +use foundry_config::{filter::expand_globs, impl_figment_convert_basic, lint::Severity}; use std::{collections::HashSet, path::PathBuf}; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] pub struct LintArgs { + /// Path to the file. + #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))] + paths: Vec, + /// The project's root path. /// /// By default root of the Git repository, if in one, @@ -18,31 +23,16 @@ pub struct LintArgs { #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")] root: Option, - /// Include only the specified files when linting. - #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] - include: Option>, - - /// Exclude the specified files when linting. - #[arg(long, value_hint = ValueHint::FilePath, value_name = "FILES", num_args(1..))] - exclude: Option>, - - /// Specifies which lints to run based on severity. + /// Specifies which lints to run based on severity. Overrides the project config. /// /// Supported values: `high`, `med`, `low`, `info`, `gas`. #[arg(long, value_name = "SEVERITY", num_args(1..))] severity: Option>, - /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). - /// - /// Cannot be used with --deny-lint. - #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..), conflicts_with = "exclude_lint")] - include_lint: Option>, - - /// Deny specific lints based on their ID (e.g., "function-mixed-case"). - /// - /// Cannot be used with --only-lint. - #[arg(long = "deny-lint", value_name = "LINT_ID", num_args(1..), conflicts_with = "include_lint")] - exclude_lint: Option>, + /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). Overrides the + /// project config. + #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))] + lint: Option>, } impl_figment_convert_basic!(LintArgs); @@ -52,50 +42,86 @@ impl LintArgs { let config = self.load_config()?; let project = config.project()?; - // Get all source files from the project - let mut sources = - project.paths.read_input_files()?.keys().cloned().collect::>(); - - // Add included paths to sources - if let Some(include_paths) = &self.include { - let included = - include_paths.iter().filter(|path| path.exists()).cloned().collect::>(); - sources.extend(included); - } - - // Remove excluded files from sources - if let Some(exclude_paths) = &self.exclude { - let excluded = exclude_paths.iter().cloned().collect::>(); - sources.retain(|path| !excluded.contains(path)); - } + // Expand ignore globs and canonicalize from the get go + let ignored = expand_globs(&config.root, config.lint.ignore.iter())? + .iter() + .flat_map(foundry_common::fs::canonicalize_path) + .collect::>(); + + let cwd = std::env::current_dir()?; + let input = match &self.paths[..] { + [] => { + // Retrieve the project paths, and filter out the ignored ones. + let project_paths = config + .project_paths::() + .input_files_iter() + .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p)))) + .collect(); + project_paths + } + paths => { + let mut inputs = Vec::with_capacity(paths.len()); + for path in paths { + if !ignored.is_empty() && + ((path.is_absolute() && ignored.contains(path)) || + ignored.contains(&cwd.join(path))) + { + continue + } + + if path.is_dir() { + inputs + .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS)); + } else if path.is_sol() { + inputs.push(path.to_path_buf()); + } else { + warn!("Cannot process path {}", path.display()); + } + } + inputs + } + }; - if sources.is_empty() { + if input.is_empty() { sh_println!("Nothing to lint")?; std::process::exit(0); } - if project.compiler.solc.is_some() { - let mut linter = SolidityLinter::new().with_severity(self.severity); - - // Resolve and apply included lints if provided - if let Some(ref include_ids) = self.include_lint { - let included_lints = include_ids - .iter() - .map(|id_str| SolLint::try_from(id_str.as_str())) - .collect::, SolLintError>>()?; - linter = linter.with_lints(Some(included_lints)); - } + // Helper to convert strings to `SolLint` objects + let convert_lints = |lints: &[String]| -> Result, SolLintError> { + lints.iter().map(|s| SolLint::try_from(s.as_str())).collect() + }; - // Resolve and apply excluded lints if provided - if let Some(ref exclude_ids) = self.exclude_lint { - let excluded_lints = exclude_ids - .iter() - .map(|id_str| SolLint::try_from(id_str.as_str())) - .collect::, SolLintError>>()?; - linter = linter.without_lints(Some(excluded_lints)); - } + // Override default lint config with user-defined lints + let (include, exclude) = if let Some(cli_lints) = &self.lint { + let include_lints = convert_lints(cli_lints)?; + let target_ids: HashSet<&str> = cli_lints.iter().map(String::as_str).collect(); + let filtered_excludes = config + .lint + .exclude_lints + .iter() + .filter(|l| !target_ids.contains(l.as_str())) + .cloned() + .collect::>(); + + (include_lints, convert_lints(&filtered_excludes)?) + } else { + (convert_lints(&config.lint.include_lints)?, convert_lints(&config.lint.exclude_lints)?) + }; + + // Override default severity config with user-defined severity + let severity = match self.severity { + Some(target) => target, + None => config.lint.severity, + }; + + if project.compiler.solc.is_some() { + let linter = SolidityLinter::new() + .with_lints(if include.is_empty() { None } else { Some(include) }) + .without_lints(if exclude.is_empty() { None } else { Some(exclude) }) + .with_severity(if severity.is_empty() { None } else { Some(severity) }); - linter.lint(&sources); + linter.lint(&input); } else { todo!("Linting not supported for this language"); }; diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index f845981bda592..7408d1d3db7c6 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -35,7 +35,7 @@ Options: -j, --threads Number of threads to use. Specifying 0 defaults to the number of logical cores - + [aliases: jobs] -V, --version @@ -58,11 +58,11 @@ Display options: -v, --verbosity... Verbosity level of the log messages. - + Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv). - + Depending on the context the verbosity levels have different meanings. - + For example, the verbosity levels of the EVM are: - 2 (-vv): Print logs for all tests. - 3 (-vvv): Print execution traces for failing tests. diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 46dd731df9e45..bb911ca99652c 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -147,6 +147,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { build_info: false, build_info_path: None, fmt: Default::default(), + lint: Default::default(), doc: Default::default(), bind_json: Default::default(), fs_permissions: Default::default(), @@ -1066,6 +1067,12 @@ ignore = [] contract_new_lines = false sort_imports = false +[lint] +severity = [] +include_lints = [] +exclude_lints = [] +ignore = [] + [doc] out = "docs" title = "" @@ -1265,6 +1272,12 @@ exclude = [] "contract_new_lines": false, "sort_imports": false }, + "lint": { + "severity": [], + "include_lints": [], + "exclude_lints": [], + "ignore": [] + }, "doc": { "out": "docs", "title": "", diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index d48eae912c050..7cbdfba2abf64 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -20,6 +20,7 @@ mod eof; mod failure_assertions; mod geiger; mod inline_config; +mod lint; mod multi_script; mod script; mod soldeer; diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 93458cebabab2..f822f7ac4299b 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -17,11 +17,13 @@ workspace = true # lib foundry-common.workspace = true foundry-compilers.workspace = true +foundry-config.workspace = true solar-parse.workspace = true solar-ast.workspace = true solar-interface.workspace = true +regex.workspace = true eyre.workspace = true rayon.workspace = true thiserror.workspace = true @@ -29,5 +31,3 @@ serde_json.workspace = true auto_impl.workspace = true yansi.workspace = true serde = { workspace = true, features = ["derive"] } -regex = "1.11" -clap = { version = "4", features = ["derive"] } diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index d75a4572193bc..a10ec4488d542 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -1,13 +1,8 @@ -use clap::ValueEnum; -use core::fmt; use foundry_compilers::Language; +use foundry_config::lint::Severity; use solar_ast::{visit::Visit, Expr, ItemFunction, ItemStruct, Span, VariableDefinition}; -use solar_interface::{ - diagnostics::{DiagBuilder, Level}, - Session, -}; +use solar_interface::{diagnostics::DiagBuilder, Session}; use std::{ops::ControlFlow, path::PathBuf}; -use yansi::Paint; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source /// code files. A linter can be implemented for any smart contract language supported by Foundry. @@ -37,49 +32,6 @@ pub trait Lint { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum Severity { - High, - Med, - Low, - Info, - Gas, -} - -impl Severity { - pub fn color(&self, message: &str) -> String { - match self { - Self::High => Paint::red(message).bold().to_string(), - Self::Med => Paint::rgb(message, 255, 135, 61).bold().to_string(), - Self::Low => Paint::yellow(message).bold().to_string(), - Self::Info => Paint::cyan(message).bold().to_string(), - Self::Gas => Paint::green(message).bold().to_string(), - } - } -} - -impl From for Level { - fn from(severity: Severity) -> Self { - match severity { - Severity::High | Severity::Med | Severity::Low => Self::Warning, - Severity::Info | Severity::Gas => Self::Note, - } - } -} - -impl fmt::Display for Severity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let colored = match self { - Self::High => self.color("High"), - Self::Med => self.color("Med"), - Self::Low => self.color("Low"), - Self::Info => self.color("Info"), - Self::Gas => self.color("Gas"), - }; - write!(f, "{colored}") - } -} - pub struct LintContext<'s> { sess: &'s Session, desc: bool, @@ -100,7 +52,9 @@ impl<'s> LintContext<'s> { let diag: DiagBuilder<'_, ()> = match lint.help() { Some(help) => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(help), - None => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(lint.description()), + None => { + self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(lint.description()) + } }; diag.emit(); diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs index 3640d97d6ed6d..c97538099f0d9 100644 --- a/crates/lint/src/sol/info.rs +++ b/crates/lint/src/sol/info.rs @@ -102,7 +102,7 @@ pub fn is_pascal_case(s: &str) -> bool { } /// Check if a string is SCREAMING_SNAKE_CASE, where -/// numbers must always be preceeded by an underscode. +/// numbers must always be preceded by an underscode. pub fn is_screaming_snake_case(s: &str) -> bool { if s.len() <= 1 { return true; @@ -110,7 +110,7 @@ pub fn is_screaming_snake_case(s: &str) -> bool { let re = Regex::new(r"^[A-Z_][A-Z0-9_]*$").unwrap(); let invalid_re = Regex::new(r"[A-Z][0-9]").unwrap(); - re.is_match(s) && !invalid_re.is_match(s) + re.is_match(s) && !invalid_re.is_match(s) } #[cfg(test)] @@ -129,8 +129,7 @@ mod test { .with_lints(Some(vec![VARIABLE_MIXED_CASE])) .with_buffer_emitter(true); - let emitted = - linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning: {}", VARIABLE_MIXED_CASE.id())).count(); let notes = emitted.matches(&format!("note: {}", VARIABLE_MIXED_CASE.id())).count(); @@ -146,8 +145,7 @@ mod test { .with_lints(Some(vec![FUNCTION_MIXED_CASE])) .with_buffer_emitter(true); - let emitted = - linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning: {}", FUNCTION_MIXED_CASE.id())).count(); let notes = emitted.matches(&format!("note: {}", FUNCTION_MIXED_CASE.id())).count(); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index c0c498a41ae8f..da17de51af592 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -3,8 +3,9 @@ pub mod high; pub mod info; pub mod med; -use crate::linter::{EarlyLintPass, EarlyLintVisitor, Lint, LintContext, Linter, Severity}; +use crate::linter::{EarlyLintPass, EarlyLintVisitor, Lint, LintContext, Linter}; use foundry_compilers::solc::SolcLanguage; +use foundry_config::lint::Severity; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena}; use solar_interface::{ From 26fa9a5061ac526ccfe578433134adbf08ce5412 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Fri, 2 May 2025 18:55:34 +0200 Subject: [PATCH 081/107] fix: broken test --- crates/forge/tests/cli/cmd.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 7408d1d3db7c6..f845981bda592 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -35,7 +35,7 @@ Options: -j, --threads Number of threads to use. Specifying 0 defaults to the number of logical cores - + [aliases: jobs] -V, --version @@ -58,11 +58,11 @@ Display options: -v, --verbosity... Verbosity level of the log messages. - + Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv). - + Depending on the context the verbosity levels have different meanings. - + For example, the verbosity levels of the EVM are: - 2 (-vv): Print logs for all tests. - 3 (-vvv): Print execution traces for failing tests. From 3adc41c07859a895ffc560c4bceedc3b14498869 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Fri, 2 May 2025 22:23:06 +0200 Subject: [PATCH 082/107] fix: fmt Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6493ebaa01973..d78822c1818a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ members = [ "crates/macros/", "crates/test-utils/", "crates/lint/", - ] resolver = "2" From 196bcb40dc4495624addbb44aca952ff20110f25 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Fri, 2 May 2025 22:23:48 +0200 Subject: [PATCH 083/107] style: naming Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- crates/forge/src/cmd/lint.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index 292e803b5dfa0..a07d20cf6d6fe 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -87,8 +87,7 @@ impl LintArgs { std::process::exit(0); } - // Helper to convert strings to `SolLint` objects - let convert_lints = |lints: &[String]| -> Result, SolLintError> { + let parse_lints = |lints: &[String]| -> Result, SolLintError> { lints.iter().map(|s| SolLint::try_from(s.as_str())).collect() }; From 311ecbb9fc709ab00acf517030fcbcb864d0eb94 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Fri, 2 May 2025 22:25:16 +0200 Subject: [PATCH 084/107] style: fmt Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- crates/lint/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index 73e1b3c95427c..e10498bdf4de7 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,5 +1,6 @@ //! # forge-lint //! //! Types, traits, and utilities for linting Solidity projects. + pub mod linter; pub mod sol; From f158f5ef66e56073e636d51bbee8bd8bebfac083 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 5 May 2025 09:50:56 +0200 Subject: [PATCH 085/107] fix: use heck + individual lint macros + housekeeping --- Cargo.lock | 455 ++++++++++++------ Cargo.toml | 14 +- crates/common/src/preprocessor/deps.rs | 2 +- crates/config/Cargo.toml | 2 +- crates/config/src/lib.rs | 1 - crates/config/src/lint.rs | 2 +- crates/forge/src/cmd/lint.rs | 17 +- crates/forge/tests/cli/lint.rs | 28 +- crates/lint/Cargo.toml | 2 +- crates/lint/src/linter.rs | 70 +-- .../lint/src/sol/{gas.rs => gas/keccack.rs} | 28 +- crates/lint/src/sol/gas/mod.rs | 9 + .../sol/{high.rs => high/incorrect_shift.rs} | 23 +- crates/lint/src/sol/high/mod.rs | 9 + crates/lint/src/sol/info.rs | 191 -------- crates/lint/src/sol/info/mixed_case.rs | 104 ++++ crates/lint/src/sol/info/mod.rs | 20 + crates/lint/src/sol/info/pascal_case.rs | 59 +++ .../lint/src/sol/info/screaming_snake_case.rs | 70 +++ crates/lint/src/sol/macros.rs | 73 +++ .../lint/src/sol/{med.rs => med/div_mul.rs} | 24 +- crates/lint/src/sol/med/mod.rs | 9 + crates/lint/src/sol/mod.rs | 149 ++---- crates/lint/testdata/MixedCase.sol | 8 +- crates/lint/testdata/ScreamingSnakeCase.sol | 3 +- crates/lint/testdata/StructPascalCase.sol | 8 +- 26 files changed, 820 insertions(+), 560 deletions(-) rename crates/lint/src/sol/{gas.rs => gas/keccack.rs} (66%) create mode 100644 crates/lint/src/sol/gas/mod.rs rename crates/lint/src/sol/{high.rs => high/incorrect_shift.rs} (78%) create mode 100644 crates/lint/src/sol/high/mod.rs delete mode 100644 crates/lint/src/sol/info.rs create mode 100644 crates/lint/src/sol/info/mixed_case.rs create mode 100644 crates/lint/src/sol/info/mod.rs create mode 100644 crates/lint/src/sol/info/pascal_case.rs create mode 100644 crates/lint/src/sol/info/screaming_snake_case.rs create mode 100644 crates/lint/src/sol/macros.rs rename crates/lint/src/sol/{med.rs => med/div_mul.rs} (76%) create mode 100644 crates/lint/src/sol/med/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5876932004fe3..42315cb0b4f4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ version = "0.1.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28e2652684758b0d9b389d248b209ed9fd9989ef489a550265fe4bb8454fe7eb" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "num_enum", "serde", "strum 0.27.1", @@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fbf458101ed6c389e9bb70a34ebc56039868ad10472540614816cdedc8f5265" dependencies = [ "alloy-eips", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-serde", "alloy-trie", @@ -105,7 +105,7 @@ checksum = "fc982af629e511292310fe85b433427fd38cb3105147632b574abc997db44c91" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-serde", "serde", @@ -119,10 +119,10 @@ checksum = "cd0a0c1ddee20ecc14308aae21c2438c994df7b39010c26d70f86e1d8fdb8db0" dependencies = [ "alloy-consensus", "alloy-dyn-abi", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-network", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-pubsub", "alloy-rpc-types-eth", @@ -139,9 +139,9 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb8e762aefd39a397ff485bc86df673465c4ad3ec8819cc60833a8a3ba5cdc87" dependencies = [ - "alloy-json-abi", - "alloy-primitives", - "alloy-sol-type-parser", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", + "alloy-sol-type-parser 0.8.25", "alloy-sol-types", "arbitrary", "const-hex", @@ -160,7 +160,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "675264c957689f0fd75f5993a73123c2cc3b5c235a38f5b9037fe6c826bfb2c0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "crc", "serde", @@ -173,7 +173,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "arbitrary", "rand 0.8.5", @@ -186,7 +186,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b15b13d38b366d01e818fe8e710d4d702ef7499eacd44926a06171dd9585d0c" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "arbitrary", "k256", @@ -204,7 +204,7 @@ dependencies = [ "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-serde", "auto_impl", @@ -223,7 +223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a40de6f5b53ecf5fd7756072942f41335426d9a3704cd961f77d854739933bcf" dependencies = [ "alloy-eips", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-serde", "alloy-trie", "serde", @@ -235,8 +235,20 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe6beff64ad0aa6ad1019a3db26fef565aefeb011736150ab73ed3366c3cfd1b" dependencies = [ - "alloy-primitives", - "alloy-sol-type-parser", + "alloy-primitives 0.8.25", + "alloy-sol-type-parser 0.8.25", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-abi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0068ae277f5ee3153a95eaea8ff10e188ed8ccde9b7f9926305415a2c0ab2442" +dependencies = [ + "alloy-primitives 1.1.0", + "alloy-sol-type-parser 1.1.0", "serde", "serde_json", ] @@ -247,7 +259,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27434beae2514d4a2aa90f53832cbdf6f23e4b5e2656d95eaf15f9276e2418b6" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-sol-types", "serde", "serde_json", @@ -266,7 +278,7 @@ dependencies = [ "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-any", "alloy-rpc-types-eth", "alloy-serde", @@ -289,7 +301,7 @@ checksum = "db973a7a23cbe96f2958e5687c51ce2d304b5c6d0dc5ccb3de8667ad8476f50b" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-serde", "serde", ] @@ -325,6 +337,33 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "alloy-primitives" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a12fe11d0b8118e551c29e1a67ccb6d01cc07ef08086df30f07487146de6fa1" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more 2.0.1", + "foldhash", + "hashbrown 0.15.2", + "indexmap 2.9.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.1", + "ruint", + "rustc-hash 2.1.1", + "serde", + "sha3", + "tiny-keccak", +] + [[package]] name = "alloy-provider" version = "0.12.6" @@ -337,7 +376,7 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-debug", @@ -375,7 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "721aca709a9231815ad5903a2d284042cc77e7d9d382696451b30c9ee0950001" dependencies = [ "alloy-json-rpc", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-transport", "bimap", "futures", @@ -416,7 +455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "445a3298c14fae7afb5b9f2f735dead989f3dd83020c2ab8e48ed95d7b6d1acb" dependencies = [ "alloy-json-rpc", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-pubsub", "alloy-transport", "alloy-transport-http", @@ -443,7 +482,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9157deaec6ba2ad7854f16146e4cd60280e76593eed79fdcb06e0fa8b6c60f77" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-anvil", "alloy-rpc-types-engine", "alloy-rpc-types-eth", @@ -459,7 +498,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a80ee83ef97e7ffd667a81ebdb6154558dfd5e8f20d8249a10a12a1671a04b3" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -482,7 +521,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08b113a0087d226291b9768ed331818fa0b0744cc1207ae7c150687cf3fde1bd" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "serde", ] @@ -494,7 +533,7 @@ checksum = "874ac9d1249ece0453e262d9ba72da9dbb3b7a2866220ded5940c2e47f1aa04d" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-serde", "derive_more 2.0.1", @@ -514,11 +553,11 @@ dependencies = [ "alloy-consensus-any", "alloy-eips", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-serde", "alloy-sol-types", - "itertools 0.14.0", + "itertools 0.13.0", "serde", "serde_json", "thiserror 2.0.12", @@ -530,7 +569,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4747763aee39c1b0f5face79bde9be8932be05b2db7d8bdcebb93490f32c889c" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -544,7 +583,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70132ebdbea1eaa68c4d6f7a62c2fadf0bdce83b904f895ab90ca4ec96f63468" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -556,7 +595,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a1cd73fc054de6353c7f22ff9b846b0f0f145cd0112da07d4119e41e9959207" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "serde", "serde_json", ] @@ -568,7 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c96fbde54bee943cd94ebacc8a62c50b38c7dfd2552dcd79ff61aea778b1bfcc" dependencies = [ "alloy-dyn-abi", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-sol-types", "async-trait", "auto_impl", @@ -586,7 +625,7 @@ checksum = "4e73835ed6689740b76cab0f59afbdce374a03d3f856ea33ba1fc054630a1b28" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-signer", "async-trait", "aws-sdk-kms", @@ -604,7 +643,7 @@ checksum = "a16b468ae86bb876d9c7a3b49b1e8d614a581a1a9673e4e0d2393b411080fe64" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-signer", "async-trait", "gcloud-sdk", @@ -623,7 +662,7 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-signer", "alloy-sol-types", "async-trait", @@ -642,7 +681,7 @@ checksum = "cc6e72002cc1801d8b41e9892165e3a6551b7bd382bd9d0414b21e90c0c62551" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-signer", "async-trait", "coins-bip32", @@ -661,7 +700,7 @@ checksum = "1d4fd403c53cf7924c3e16c61955742cfc3813188f0975622f4fa6f8a01760aa" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-signer", "async-trait", "semver 1.0.26", @@ -690,7 +729,7 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" dependencies = [ - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-sol-macro-input", "const-hex", "heck", @@ -709,7 +748,7 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" dependencies = [ - "alloy-json-abi", + "alloy-json-abi 0.8.25", "const-hex", "dunce", "heck", @@ -731,14 +770,24 @@ dependencies = [ "winnow 0.7.7", ] +[[package]] +name = "alloy-sol-type-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251273c5aa1abb590852f795c938730fa641832fc8fa77b5478ed1bf11b6097e" +dependencies = [ + "serde", + "winnow 0.7.7", +] + [[package]] name = "alloy-sol-types" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" dependencies = [ - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "alloy-sol-macro", "const-hex", "serde", @@ -825,7 +874,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "arrayvec", "derive_more 1.0.0", @@ -957,7 +1006,7 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-pubsub", "alloy-rlp", @@ -1014,7 +1063,7 @@ dependencies = [ "alloy-dyn-abi", "alloy-eips", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-rpc-types", "alloy-serde", @@ -2089,10 +2138,10 @@ dependencies = [ "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-json-rpc", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rlp", "alloy-rpc-types", @@ -2182,8 +2231,8 @@ name = "chisel" version = "1.1.0" dependencies = [ "alloy-dyn-abi", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "clap", "dirs", "eyre", @@ -2201,7 +2250,7 @@ dependencies = [ "serde", "serde_json", "solang-parser", - "solar-parse", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "strum 0.27.1", "tikv-jemallocator", "time", @@ -3248,7 +3297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3308,7 +3357,7 @@ checksum = "78329cbf3c326a3ce2694003976c019fe5f407682b1fdc76e89e463826ea511a" dependencies = [ "ahash", "alloy-dyn-abi", - "alloy-primitives", + "alloy-primitives 0.8.25", "indexmap 2.9.0", ] @@ -3367,7 +3416,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.5", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3464,9 +3513,9 @@ version = "1.1.0" dependencies = [ "alloy-chains", "alloy-dyn-abi", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rpc-types", "alloy-serde", @@ -3522,8 +3571,8 @@ dependencies = [ "similar", "similar-asserts", "solang-parser", - "solar-interface", - "solar-parse", + "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "soldeer-commands", "strum 0.27.1", "svm-rs", @@ -3545,7 +3594,7 @@ dependencies = [ name = "forge-doc" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "derive_more 2.0.1", "eyre", "forge-fmt", @@ -3568,7 +3617,7 @@ dependencies = [ name = "forge-fmt" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "ariadne", "foundry-config", "itertools 0.14.0", @@ -3590,13 +3639,13 @@ dependencies = [ "foundry-common", "foundry-compilers", "foundry-config", + "heck", "rayon", - "regex", "serde", "serde_json", - "solar-ast", - "solar-interface", - "solar-parse", + "solar-ast 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "thiserror 2.0.12", "yansi", ] @@ -3609,9 +3658,9 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-eips", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rpc-types", "alloy-serde", @@ -3650,7 +3699,7 @@ name = "forge-script-sequence" version = "1.1.0" dependencies = [ "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "eyre", "foundry-common", "foundry-compilers", @@ -3681,8 +3730,8 @@ name = "forge-verify" version = "1.1.0" dependencies = [ "alloy-dyn-abi", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rpc-types", "async-trait", @@ -3725,8 +3774,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "001678abc9895502532c8c4a1a225079c580655fc82a194e78b06dcf99f49b8c" dependencies = [ "alloy-chains", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "foundry-compilers", "reqwest", "semver 1.0.26", @@ -3744,9 +3793,9 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-genesis", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rlp", "alloy-rpc-types", @@ -3802,8 +3851,8 @@ dependencies = [ "alloy-chains", "alloy-dyn-abi", "alloy-eips", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rlp", "clap", @@ -3843,10 +3892,10 @@ dependencies = [ "alloy-contract", "alloy-dyn-abi", "alloy-eips", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-json-rpc", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", @@ -3879,8 +3928,8 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", - "solar-parse", - "solar-sema", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-sema 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "terminal_size", "thiserror 2.0.12", "tokio", @@ -3899,7 +3948,7 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types", "alloy-serde", "chrono", @@ -3918,8 +3967,8 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94bb4155f53d4b05642a1398ad105dc04d44b368a7932b85f6ed012af48768b7" dependencies = [ - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "auto_impl", "derive_more 1.0.0", "dirs", @@ -3929,7 +3978,7 @@ dependencies = [ "fs_extra", "futures-util", "home", - "itertools 0.14.0", + "itertools 0.13.0", "path-slash", "rand 0.8.5", "rayon", @@ -3937,8 +3986,8 @@ dependencies = [ "serde", "serde_json", "sha2", - "solar-parse", - "solar-sema", + "solar-parse 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-sema 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "svm-rs", "svm-rs-builds", "tempfile", @@ -3965,8 +4014,8 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d5c80fda7c4fde0d2964b329b22d09718838da0c940e5df418f2c1db14fd24" dependencies = [ - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "foundry-compilers-core", "futures-util", "path-slash", @@ -3988,8 +4037,8 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eaf3cad3dd7bd9eae02736e98f55aaf00ee31fbc0a367613436c2fb01c43914" dependencies = [ - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "foundry-compilers-artifacts-solc", "foundry-compilers-core", "path-slash", @@ -4003,7 +4052,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac5a1aef4083544309765a1a10c310dffde8c9b8bcfda79b7c2bcfde32f3be3" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "cfg-if", "dunce", "fs_extra", @@ -4025,7 +4074,7 @@ name = "foundry-config" version = "1.1.0" dependencies = [ "alloy-chains", - "alloy-primitives", + "alloy-primitives 0.8.25", "clap", "dirs", "dunce", @@ -4047,8 +4096,8 @@ dependencies = [ "serde", "serde_json", "similar-asserts", - "solar-interface", - "solar-parse", + "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "soldeer-core", "tempfile", "thiserror 2.0.12", @@ -4063,7 +4112,7 @@ dependencies = [ name = "foundry-debugger" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "crossterm", "eyre", "foundry-common", @@ -4082,8 +4131,8 @@ name = "foundry-evm" version = "1.1.0" dependencies = [ "alloy-dyn-abi", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "alloy-sol-types", "eyre", "foundry-cheatcodes", @@ -4108,7 +4157,7 @@ dependencies = [ name = "foundry-evm-abi" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-sol-types", "derive_more 2.0.1", "foundry-common-fmt", @@ -4123,9 +4172,9 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-genesis", - "alloy-json-abi", + "alloy-json-abi 0.8.25", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rpc-types", "alloy-sol-types", @@ -4154,7 +4203,7 @@ dependencies = [ name = "foundry-evm-coverage" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "eyre", "foundry-common", "foundry-compilers", @@ -4170,8 +4219,8 @@ name = "foundry-evm-fuzz" version = "1.1.0" dependencies = [ "alloy-dyn-abi", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "eyre", "foundry-common", "foundry-compilers", @@ -4194,8 +4243,8 @@ name = "foundry-evm-traces" version = "1.1.0" dependencies = [ "alloy-dyn-abi", - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "alloy-sol-types", "eyre", "foundry-block-explorers", @@ -4211,7 +4260,7 @@ dependencies = [ "revm-inspectors", "serde", "serde_json", - "solar-parse", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "tempfile", "tokio", "tracing", @@ -4224,7 +4273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba7beb856e73f59015823eb221a98b7c22b58bc4e7066c9c86774ebe74e61dd6" dependencies = [ "alloy-consensus", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "alloy-rpc-types", "eyre", @@ -4243,7 +4292,7 @@ dependencies = [ name = "foundry-linking" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "foundry-compilers", "semver 1.0.26", "thiserror 2.0.12", @@ -4263,7 +4312,7 @@ dependencies = [ name = "foundry-test-utils" version = "1.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-provider", "eyre", "fd-lock", @@ -4290,7 +4339,7 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-signer", "alloy-signer-aws", "alloy-signer-gcp", @@ -5379,7 +5428,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5451,7 +5500,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6297,7 +6346,7 @@ checksum = "889facbf449b2d9c8de591cd467a6c7217936f3c1c07a281759c01c49d08d66d" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "alloy-serde", "derive_more 2.0.1", @@ -6314,7 +6363,7 @@ dependencies = [ "alloy-consensus", "alloy-eips", "alloy-network-primitives", - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-eth", "alloy-serde", "derive_more 2.0.1", @@ -6926,7 +6975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.101", @@ -7069,7 +7118,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7123,6 +7172,7 @@ checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", + "serde", ] [[package]] @@ -7161,6 +7211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.2", + "serde", ] [[package]] @@ -7375,7 +7426,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6a43423d81f4bef634469bfb2d9ebe36a9ea9167f20ab3a7d1ff1e05fa63099" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-sol-types", @@ -7425,7 +7476,7 @@ checksum = "f0f987564210317706def498421dfba2ae1af64a8edce82c6102758b48133fcb" dependencies = [ "alloy-eip2930", "alloy-eip7702", - "alloy-primitives", + "alloy-primitives 0.8.25", "auto_impl", "bitflags 2.9.0", "bitvec", @@ -7609,7 +7660,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7622,7 +7673,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8299,16 +8350,34 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0a583a12e73099d1f54bfe7c8a30d7af5ff3591c61ee51cce91045ee5496d86" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "bumpalo", "derive_more 2.0.1", "either", "num-bigint", "num-rational", "semver 1.0.26", - "solar-data-structures", - "solar-interface", - "solar-macros", + "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-interface 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "strum 0.27.1", + "typed-arena", +] + +[[package]] +name = "solar-ast" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "alloy-primitives 1.1.0", + "bumpalo", + "either", + "num-bigint", + "num-rational", + "semver 1.0.26", + "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-macros 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "strum 0.27.1", "typed-arena", ] @@ -8322,6 +8391,14 @@ dependencies = [ "strum 0.27.1", ] +[[package]] +name = "solar-config" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "strum 0.27.1", +] + [[package]] name = "solar-data-structures" version = "0.1.2" @@ -8337,6 +8414,20 @@ dependencies = [ "smallvec", ] +[[package]] +name = "solar-data-structures" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "bumpalo", + "index_vec", + "indexmap 2.9.0", + "parking_lot", + "rayon", + "rustc-hash 2.1.1", + "smallvec", +] + [[package]] name = "solar-interface" version = "0.1.2" @@ -8350,7 +8441,7 @@ dependencies = [ "derive_builder", "derive_more 2.0.1", "dunce", - "itertools 0.14.0", + "itertools 0.10.5", "itoa", "lasso", "match_cfg", @@ -8358,10 +8449,37 @@ dependencies = [ "rayon", "scc", "scoped-tls", - "solar-config", - "solar-data-structures", - "solar-macros", - "thiserror 2.0.12", + "solar-config 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.69", + "tracing", + "unicode-width 0.2.0", +] + +[[package]] +name = "solar-interface" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "annotate-snippets", + "anstream", + "anstyle", + "const-hex", + "derive_builder", + "derive_more 2.0.1", + "dunce", + "itertools 0.10.5", + "itoa", + "lasso", + "match_cfg", + "normalize-path", + "rayon", + "scoped-tls", + "solar-config 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-macros 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "thiserror 1.0.69", "tracing", "unicode-width 0.2.0", ] @@ -8377,24 +8495,54 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "solar-macros" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "solar-parse" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1bc1d0253b0f7f2c7cd25ed7bc5d5e8cac43e717d002398250e0e66e43278b" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "bitflags 2.9.0", "bumpalo", - "itertools 0.14.0", + "itertools 0.10.5", + "memchr", + "num-bigint", + "num-rational", + "num-traits", + "smallvec", + "solar-ast 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-interface 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", +] + +[[package]] +name = "solar-parse" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "alloy-primitives 1.1.0", + "bitflags 2.9.0", + "bumpalo", + "itertools 0.10.5", "memchr", "num-bigint", "num-rational", "num-traits", "smallvec", - "solar-ast", - "solar-data-structures", - "solar-interface", + "solar-ast 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "tracing", ] @@ -8404,8 +8552,8 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded4b26fb85a0ae2f3277377236af0884c82f38965a2c51046a53016c8b5f332" dependencies = [ - "alloy-json-abi", - "alloy-primitives", + "alloy-json-abi 0.8.25", + "alloy-primitives 0.8.25", "bitflags 2.9.0", "bumpalo", "derive_more 2.0.1", @@ -8416,11 +8564,38 @@ dependencies = [ "scc", "serde", "serde_json", - "solar-ast", - "solar-data-structures", - "solar-interface", - "solar-macros", - "solar-parse", + "solar-ast 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-interface 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-parse 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "strum 0.27.1", + "thread_local", + "tracing", + "typed-arena", +] + +[[package]] +name = "solar-sema" +version = "0.1.2" +source = "git+https://github.com/paradigmxyz/solar?branch=main#65186e7a3b2b6e1412d6ca8d544f9622311a1e81" +dependencies = [ + "alloy-json-abi 1.1.0", + "alloy-primitives 1.1.0", + "bitflags 2.9.0", + "bumpalo", + "derive_more 2.0.1", + "either", + "once_map", + "paste", + "rayon", + "serde", + "serde_json", + "solar-ast 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-macros 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", "strum 0.27.1", "thread_local", "tracing", @@ -8707,7 +8882,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 2.0.12", + "thiserror 1.0.69", "url", "zip", ] @@ -8805,7 +8980,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.5", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -10033,7 +10208,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d78822c1818a4..fe35b05651ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,10 +194,15 @@ foundry-block-explorers = { version = "0.13.0", default-features = false } foundry-compilers = { version = "0.14.0", default-features = false } foundry-fork-db = "0.12" solang-parser = "=0.3.3" -solar-ast = { version = "=0.1.2", default-features = false } -solar-parse = { version = "=0.1.2", default-features = false } -solar-interface = { version = "=0.1.2", default-features = false } -solar-sema = { version = "=0.1.2", default-features = false } +# TODO: enable again after 0.1.3 release +# solar-ast = { version = "=0.1.2", default-features = false } +# solar-parse = { version = "=0.1.2", default-features = false } +# solar-interface = { version = "=0.1.2", default-features = false } +# solar-sema = { version = "=0.1.2", default-features = false } +solar-ast = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-ast" } +solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-parse" } +solar-interface = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-interface" } +solar-sema = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-sema" } ## revm revm = { version = "19.4.0", default-features = false } @@ -321,6 +326,7 @@ vergen = { version = "8", default-features = false } yansi = { version = "1.0", features = ["detect-tty", "detect-env"] } path-slash = "0.2" jiff = "0.2" +heck = "0.5" # Use unicode-rs which has a smaller binary size than the default ICU4X as the IDNA backend, used # by the `url` crate. diff --git a/crates/common/src/preprocessor/deps.rs b/crates/common/src/preprocessor/deps.rs index e15e21798e9af..dbcfd1b762f33 100644 --- a/crates/common/src/preprocessor/deps.rs +++ b/crates/common/src/preprocessor/deps.rs @@ -194,7 +194,7 @@ impl<'hir> Visit<'hir> for BytecodeDependencyCollector<'hir> { // the offset will be used to replace `{value: 333} ( ` with `(` let call_args_offset = if named_args.is_some() && !call_args.is_empty() { - (call_args.span().lo() - ty_new.span.hi()).to_usize() + (call_args.span.lo() - ty_new.span.hi()).to_usize() } else { 0 }; diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index d5897fd89ad4a..1b8a787a9cd36 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -30,7 +30,7 @@ eyre.workspace = true figment = { workspace = true, features = ["toml", "env"] } glob = "0.3" globset = "0.4" -heck = "0.5" +heck.workspace = true itertools.workspace = true mesc.workspace = true number_prefix = "0.4" diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 612319f15a99a..944464a4316e5 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -4448,7 +4448,6 @@ mod tests { r" [lint] severity = ['high', 'medium'] - include_lints = ['function-mixed-case'] exclude_lints = ['incorrect-shift'] ", )?; diff --git a/crates/config/src/lint.rs b/crates/config/src/lint.rs index d1420ab3a82f2..b5dc7dc315d93 100644 --- a/crates/config/src/lint.rs +++ b/crates/config/src/lint.rs @@ -20,7 +20,7 @@ pub struct LinterConfig { /// Cannot be used in combination with `exclude_lint`. pub include_lints: Vec, - /// Deny specific lints based on their ID (e.g., "function-mixed-case"). + /// Deny specific lints based on their ID (e.g. "mixed-case-function"). /// /// Cannot be used in combination with `include_lint`. pub exclude_lints: Vec, diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index a07d20cf6d6fe..d41f0751aa55b 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -60,15 +60,9 @@ impl LintArgs { project_paths } paths => { + // Override default excluded paths and only lint the input files. let mut inputs = Vec::with_capacity(paths.len()); for path in paths { - if !ignored.is_empty() && - ((path.is_absolute() && ignored.contains(path)) || - ignored.contains(&cwd.join(path))) - { - continue - } - if path.is_dir() { inputs .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS)); @@ -84,7 +78,7 @@ impl LintArgs { if input.is_empty() { sh_println!("Nothing to lint")?; - std::process::exit(0); + return Ok(()); } let parse_lints = |lints: &[String]| -> Result, SolLintError> { @@ -93,7 +87,7 @@ impl LintArgs { // Override default lint config with user-defined lints let (include, exclude) = if let Some(cli_lints) = &self.lint { - let include_lints = convert_lints(cli_lints)?; + let include_lints = parse_lints(cli_lints)?; let target_ids: HashSet<&str> = cli_lints.iter().map(String::as_str).collect(); let filtered_excludes = config .lint @@ -103,9 +97,9 @@ impl LintArgs { .cloned() .collect::>(); - (include_lints, convert_lints(&filtered_excludes)?) + (include_lints, parse_lints(&filtered_excludes)?) } else { - (convert_lints(&config.lint.include_lints)?, convert_lints(&config.lint.exclude_lints)?) + (parse_lints(&config.lint.include_lints)?, parse_lints(&config.lint.exclude_lints)?) }; // Override default severity config with user-defined severity @@ -116,6 +110,7 @@ impl LintArgs { if project.compiler.solc.is_some() { let linter = SolidityLinter::new() + .with_description(true) .with_lints(if include.is_empty() { None } else { Some(include) }) .without_lints(if exclude.is_empty() { None } else { Some(exclude) }) .with_severity(if severity.is_empty() { None } else { Some(severity) }); diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index 6f355bfdcd25d..1e80dff53ff72 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -10,7 +10,7 @@ uint256 constant screaming_snake_case_info = 0; contract ContractWithLints { uint256 VARIABLE_MIXED_CASE_INFO; - function incorrectShitHigh() public { + function incorrectShiftHigh() public { uint256 localValue = 50; result = 8 >> localValue; } @@ -48,13 +48,13 @@ forgetest!(can_use_config, |prj, cmd| { ..Default::default() }); cmd.arg("lint").assert_success().stderr_eq(str![[r#" -warning: divide-before-multiply +warning[divide-before-multiply] [FILE]:16:9 | 16 | (1 / 2) * 3; | ----------- | - = help: Multiplication should occur before division to avoid loss of precision + = help: multiplication should occur before division to avoid loss of precision "#]]); @@ -74,13 +74,13 @@ forgetest!(can_use_config_ignore, |prj, cmd| { ..Default::default() }); cmd.arg("lint").assert_success().stderr_eq(str![[r#" -note: variable-mixed-case +note[mixed-case-variable] [FILE]:6:9 | 6 | uint256 VARIABLE_MIXED_CASE_INFO; | --------------------------------- | - = help: Mutable variables should use mixedCase + = help: mutable variables should use mixedCase "#]]); @@ -91,7 +91,7 @@ forgetest!(can_override_config_severity, |prj, cmd| { prj.add_source("ContractWithLints", CONTRACT).unwrap(); prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); - // Check config for `severity` and `exclude` + // Override severity prj.write_config(Config { lint: LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], @@ -101,13 +101,13 @@ forgetest!(can_override_config_severity, |prj, cmd| { ..Default::default() }); cmd.arg("lint").args(["--severity", "info"]).assert_success().stderr_eq(str![[r#" -note: variable-mixed-case +note[mixed-case-variable] [FILE]:6:9 | 6 | uint256 VARIABLE_MIXED_CASE_INFO; | --------------------------------- | - = help: Mutable variables should use mixedCase + = help: mutable variables should use mixedCase "#]]); @@ -118,7 +118,7 @@ forgetest!(can_override_config_path, |prj, cmd| { prj.add_source("ContractWithLints", CONTRACT).unwrap(); prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); - // Check config for `severity` and `exclude` + // Override excluded files prj.write_config(Config { lint: LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], @@ -129,13 +129,13 @@ forgetest!(can_override_config_path, |prj, cmd| { ..Default::default() }); cmd.arg("lint").arg("src/ContractWithLints.sol").assert_success().stderr_eq(str![[r#" -warning: divide-before-multiply +warning[divide-before-multiply] [FILE]:16:9 | 16 | (1 / 2) * 3; | ----------- | - = help: Multiplication should occur before division to avoid loss of precision + = help: multiplication should occur before division to avoid loss of precision "#]]); @@ -146,7 +146,7 @@ forgetest!(can_override_config_lint, |prj, cmd| { prj.add_source("ContractWithLints", CONTRACT).unwrap(); prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); - // Check config for `severity` and `exclude` + // Override excluded lints prj.write_config(Config { lint: LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], @@ -157,13 +157,13 @@ forgetest!(can_override_config_lint, |prj, cmd| { }); cmd.arg("lint").args(["--only-lint", "incorrect-shift"]).assert_success().stderr_eq(str![[ r#" -warning: incorrect-shift +warning[incorrect-shift] [FILE]:13:18 | 13 | result = 8 >> localValue; | --------------- | - = help: The order of args in a shift operation is incorrect + = help: the order of args in a shift operation is incorrect "# diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index ca435c2cf136f..7f9e4a84f59b9 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -23,7 +23,7 @@ solar-parse.workspace = true solar-ast.workspace = true solar-interface.workspace = true -regex.workspace = true +heck.workspace = true eyre.workspace = true rayon.workspace = true thiserror.workspace = true diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index a10ec4488d542..0efb7a9eebf8a 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -1,7 +1,11 @@ use foundry_compilers::Language; use foundry_config::lint::Severity; -use solar_ast::{visit::Visit, Expr, ItemFunction, ItemStruct, Span, VariableDefinition}; -use solar_interface::{diagnostics::DiagBuilder, Session}; +use solar_ast::{visit::Visit, Expr, ItemFunction, ItemStruct, VariableDefinition}; +use solar_interface::{ + data_structures::Never, + diagnostics::{DiagBuilder, DiagId, MultiSpan}, + Session, Span, +}; use std::{ops::ControlFlow, path::PathBuf}; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source @@ -44,18 +48,19 @@ impl<'s> LintContext<'s> { // Helper method to emit diagnostics easily from passes pub fn emit(&self, lint: &'static L, span: Span) { - let msg = if self.desc && lint.help().is_some() { - format!("{}\n --> {}", lint.id(), lint.description()) - } else { - lint.id().into() + let (desc, help) = match (self.desc, lint.help()) { + (true, Some(help)) => (lint.description(), help), + (true, None) => ("", lint.description()), + (false, _) => ("", ""), }; - let diag: DiagBuilder<'_, ()> = match lint.help() { - Some(help) => self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(help), - None => { - self.sess.dcx.diag(lint.severity().into(), msg).span(span).help(lint.description()) - } - }; + let diag: DiagBuilder<'_, ()> = self + .sess + .dcx + .diag(lint.severity().into(), desc) + .code(DiagId::new_str(lint.id())) + .span(MultiSpan::from_span(span)) + .help(help); diag.emit(); } @@ -64,29 +69,14 @@ impl<'s> LintContext<'s> { /// Trait for lints that operate directly on the AST. /// Its methods mirror `solar_ast::visit::Visit`, with the addition of `LintCotext`. pub trait EarlyLintPass<'ast>: Send + Sync { - fn check_expr(&mut self, _ctx: &LintContext<'_>, _expr: &'ast Expr<'ast>) -> ControlFlow<()> { - ControlFlow::Continue(()) - } + fn check_expr(&mut self, _ctx: &LintContext<'_>, _expr: &'ast Expr<'ast>) {} + fn check_item_struct(&mut self, _ctx: &LintContext<'_>, _struct: &'ast ItemStruct<'ast>) {} + fn check_item_function(&mut self, _ctx: &LintContext<'_>, _func: &'ast ItemFunction<'ast>) {} fn check_variable_definition( &mut self, _ctx: &LintContext<'_>, _var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow<()> { - ControlFlow::Continue(()) - } - fn check_item_struct( - &mut self, - _ctx: &LintContext<'_>, - _struct: &'ast ItemStruct<'ast>, - ) -> ControlFlow<()> { - ControlFlow::Continue(()) - } - fn check_item_function( - &mut self, - _ctx: &LintContext<'_>, - _func: &'ast ItemFunction<'ast>, - ) -> ControlFlow<()> { - ControlFlow::Continue(()) + ) { } // TODO: Add methods for each required AST node type @@ -102,13 +92,11 @@ impl<'s, 'ast> Visit<'ast> for EarlyLintVisitor<'_, 's, 'ast> where 's: 'ast, { - type BreakValue = (); + type BreakValue = Never; fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow { for pass in self.passes.iter_mut() { - if let ControlFlow::Break(_) = pass.check_expr(self.ctx, expr) { - return ControlFlow::Break(()); - } + pass.check_expr(self.ctx, expr) } self.walk_expr(expr) } @@ -118,9 +106,7 @@ where var: &'ast VariableDefinition<'ast>, ) -> ControlFlow { for pass in self.passes.iter_mut() { - if let ControlFlow::Break(_) = pass.check_variable_definition(self.ctx, var) { - return ControlFlow::Break(()); - } + pass.check_variable_definition(self.ctx, var) } self.walk_variable_definition(var) } @@ -130,9 +116,7 @@ where strukt: &'ast ItemStruct<'ast>, ) -> ControlFlow { for pass in self.passes.iter_mut() { - if let ControlFlow::Break(_) = pass.check_item_struct(self.ctx, strukt) { - return ControlFlow::Break(()); - } + pass.check_item_struct(self.ctx, strukt) } self.walk_item_struct(strukt) } @@ -142,9 +126,7 @@ where func: &'ast ItemFunction<'ast>, ) -> ControlFlow { for pass in self.passes.iter_mut() { - if let ControlFlow::Break(_) = pass.check_item_function(self.ctx, func) { - return ControlFlow::Break(()); - } + pass.check_item_function(self.ctx, func) } self.walk_item_function(func) } diff --git a/crates/lint/src/sol/gas.rs b/crates/lint/src/sol/gas/keccack.rs similarity index 66% rename from crates/lint/src/sol/gas.rs rename to crates/lint/src/sol/gas/keccack.rs index 38dc6b113f16b..c3259fd81c8ff 100644 --- a/crates/lint/src/sol/gas.rs +++ b/crates/lint/src/sol/gas/keccack.rs @@ -1,16 +1,23 @@ use solar_ast::{Expr, ExprKind}; use solar_interface::kw::Keccak256; -use std::ops::ControlFlow; -use super::{AsmKeccak256, ASM_KECCACK256}; -use crate::linter::EarlyLintPass; +use super::AsmKeccak256; +use crate::{ + declare_forge_lint, + linter::EarlyLintPass, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + ASM_KECCACK256, + Severity::Gas, + "asm-keccack256", + "hash using inline assembly to save gas", + "" +); impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { - fn check_expr( - &mut self, - ctx: &crate::linter::LintContext<'_>, - expr: &'ast Expr<'ast>, - ) -> ControlFlow<()> { + fn check_expr(&mut self, ctx: &crate::linter::LintContext<'_>, expr: &'ast Expr<'ast>) { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { if ident.name == Keccak256 { @@ -18,7 +25,6 @@ impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { } } } - ControlFlow::Continue(()) } } @@ -35,8 +41,8 @@ mod test { SolidityLinter::new().with_lints(Some(vec![ASM_KECCACK256])).with_buffer_emitter(true); let emitted = linter.lint_file(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", ASM_KECCACK256.id())).count(); - let notes = emitted.matches(&format!("note: {}", ASM_KECCACK256.id())).count(); + let warnings = emitted.matches(&format!("warning[{}]", ASM_KECCACK256.id())).count(); + let notes = emitted.matches(&format!("note[{}]", ASM_KECCACK256.id())).count(); assert_eq!(warnings, 0, "Expected 0 warnings"); assert_eq!(notes, 2, "Expected 2 notes"); diff --git a/crates/lint/src/sol/gas/mod.rs b/crates/lint/src/sol/gas/mod.rs new file mode 100644 index 0000000000000..916ae2b34dca3 --- /dev/null +++ b/crates/lint/src/sol/gas/mod.rs @@ -0,0 +1,9 @@ +mod keccack; +use keccack::ASM_KECCACK256; + +use crate::{ + register_lints, + sol::{EarlyLintPass, SolLint}, +}; + +register_lints!((AsmKeccak256, ASM_KECCACK256)); diff --git a/crates/lint/src/sol/high.rs b/crates/lint/src/sol/high/incorrect_shift.rs similarity index 78% rename from crates/lint/src/sol/high.rs rename to crates/lint/src/sol/high/incorrect_shift.rs index 116528ba19b73..f680630c5e49d 100644 --- a/crates/lint/src/sol/high.rs +++ b/crates/lint/src/sol/high/incorrect_shift.rs @@ -1,10 +1,22 @@ use solar_ast::{BinOp, BinOpKind, Expr, ExprKind}; -use std::ops::ControlFlow; -use super::{EarlyLintPass, IncorrectShift, LintContext, INCORRECT_SHIFT}; +use super::IncorrectShift; +use crate::{ + declare_forge_lint, + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + INCORRECT_SHIFT, + Severity::High, + "incorrect-shift", + "the order of args in a shift operation is incorrect", + "" +); impl<'ast> EarlyLintPass<'ast> for IncorrectShift { - fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) -> ControlFlow<()> { + fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) { if let ExprKind::Binary( left_expr, BinOp { kind: BinOpKind::Shl | BinOpKind::Shr, .. }, @@ -15,7 +27,6 @@ impl<'ast> EarlyLintPass<'ast> for IncorrectShift { ctx.emit(&INCORRECT_SHIFT, expr.span); } } - ControlFlow::Continue(()) } } @@ -46,8 +57,8 @@ mod test { let emitted = linter.lint_file(Path::new("testdata/IncorrectShift.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", INCORRECT_SHIFT.id())).count(); - let notes = emitted.matches(&format!("note: {}", INCORRECT_SHIFT.id())).count(); + let warnings = emitted.matches(&format!("warning[{}]", INCORRECT_SHIFT.id())).count(); + let notes = emitted.matches(&format!("note[{}]", INCORRECT_SHIFT.id())).count(); assert_eq!(warnings, 5, "Expected 5 warnings"); assert_eq!(notes, 0, "Expected 0 notes"); diff --git a/crates/lint/src/sol/high/mod.rs b/crates/lint/src/sol/high/mod.rs new file mode 100644 index 0000000000000..e6517bdcf9adb --- /dev/null +++ b/crates/lint/src/sol/high/mod.rs @@ -0,0 +1,9 @@ +mod incorrect_shift; +use incorrect_shift::INCORRECT_SHIFT; + +use crate::{ + register_lints, + sol::{EarlyLintPass, SolLint}, +}; + +register_lints!((IncorrectShift, INCORRECT_SHIFT)); diff --git a/crates/lint/src/sol/info.rs b/crates/lint/src/sol/info.rs deleted file mode 100644 index c97538099f0d9..0000000000000 --- a/crates/lint/src/sol/info.rs +++ /dev/null @@ -1,191 +0,0 @@ -use regex::Regex; -use solar_ast::{ItemFunction, ItemStruct, VariableDefinition}; -use std::ops::ControlFlow; - -use crate::{ - linter::{EarlyLintPass, LintContext}, - sol::{ - FunctionMixedCase, ScreamingSnakeCase, StructPascalCase, VariableMixedCase, - FUNCTION_MIXED_CASE, SCREAMING_SNAKE_CASE, STRUCT_PASCAL_CASE, VARIABLE_MIXED_CASE, - }, -}; - -impl<'ast> EarlyLintPass<'ast> for VariableMixedCase { - fn check_variable_definition( - &mut self, - ctx: &LintContext<'_>, - var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow<()> { - if var.mutability.is_none() { - if let Some(name) = var.name { - let name = name.as_str(); - if !is_mixed_case(name) { - ctx.emit(&VARIABLE_MIXED_CASE, var.span); - } - } - } - ControlFlow::Continue(()) - } -} - -impl<'ast> EarlyLintPass<'ast> for FunctionMixedCase { - fn check_item_function( - &mut self, - ctx: &LintContext<'_>, - func: &'ast ItemFunction<'ast>, - ) -> ControlFlow<()> { - if let Some(name) = func.header.name { - let name = name.as_str(); - if !is_mixed_case(name) && name.len() > 1 { - ctx.emit(&FUNCTION_MIXED_CASE, func.body_span); - } - } - ControlFlow::Continue(()) - } -} - -impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { - fn check_variable_definition( - &mut self, - ctx: &LintContext<'_>, - var: &'ast VariableDefinition<'ast>, - ) -> ControlFlow<()> { - if let Some(mutability) = var.mutability { - if mutability.is_constant() || mutability.is_immutable() { - if let Some(name) = var.name { - let name = name.as_str(); - if !is_screaming_snake_case(name) && name.len() > 1 { - ctx.emit(&SCREAMING_SNAKE_CASE, var.span); - } - } - } - } - ControlFlow::Continue(()) - } -} - -impl<'ast> EarlyLintPass<'ast> for StructPascalCase { - fn check_item_struct( - &mut self, - ctx: &LintContext<'_>, - strukt: &'ast ItemStruct<'ast>, - ) -> ControlFlow<()> { - let name = strukt.name.as_str(); - if !is_pascal_case(name) && name.len() > 1 { - ctx.emit(&STRUCT_PASCAL_CASE, strukt.name.span); - } - ControlFlow::Continue(()) - } -} - -/// Check if a string is mixedCase -/// -/// To avoid false positives like `fn increment()` or `uin256 counter`, -/// lowercase strings are treated as mixedCase. -pub fn is_mixed_case(s: &str) -> bool { - if s.len() <= 1 { - return true; - } - - let re = Regex::new(r"^[a-z_][a-zA-Z0-9]*$").unwrap(); - re.is_match(s) -} - -/// Check if a string is PascalCase -pub fn is_pascal_case(s: &str) -> bool { - if s.len() <= 1 { - return true; - } - - let re = Regex::new(r"^[A-Z][a-z]+(?:[A-Z][a-z]+)*$").unwrap(); - re.is_match(s) -} - -/// Check if a string is SCREAMING_SNAKE_CASE, where -/// numbers must always be preceded by an underscode. -pub fn is_screaming_snake_case(s: &str) -> bool { - if s.len() <= 1 { - return true; - } - - let re = Regex::new(r"^[A-Z_][A-Z0-9_]*$").unwrap(); - let invalid_re = Regex::new(r"[A-Z][0-9]").unwrap(); - re.is_match(s) && !invalid_re.is_match(s) -} - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{ - linter::Lint, - sol::{SolidityLinter, FUNCTION_MIXED_CASE}, - }; - - #[test] - fn test_variable_mixed_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![VARIABLE_MIXED_CASE])) - .with_buffer_emitter(true); - - let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", VARIABLE_MIXED_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", VARIABLE_MIXED_CASE.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 5, "Expected 5 notes"); - - Ok(()) - } - - #[test] - fn test_function_mixed_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![FUNCTION_MIXED_CASE])) - .with_buffer_emitter(true); - - let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", FUNCTION_MIXED_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", FUNCTION_MIXED_CASE.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 3, "Expected 3 notes"); - - Ok(()) - } - - #[test] - fn test_screaming_snake_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![SCREAMING_SNAKE_CASE])) - .with_buffer_emitter(true); - - let emitted = - linter.lint_file(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", SCREAMING_SNAKE_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", SCREAMING_SNAKE_CASE.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 9, "Expected 9 notes"); - - Ok(()) - } - - #[test] - fn test_struct_pascal_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![STRUCT_PASCAL_CASE])) - .with_buffer_emitter(true); - - let emitted = - linter.lint_file(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning: {}", STRUCT_PASCAL_CASE.id())).count(); - let notes = emitted.matches(&format!("note: {}", STRUCT_PASCAL_CASE.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 7, "Expected 7 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/info/mixed_case.rs b/crates/lint/src/sol/info/mixed_case.rs new file mode 100644 index 0000000000000..cf076fe3b7a08 --- /dev/null +++ b/crates/lint/src/sol/info/mixed_case.rs @@ -0,0 +1,104 @@ +use solar_ast::{ItemFunction, VariableDefinition}; + +use super::{MixedCaseFunction, MixedCaseVariable}; +use crate::{ + declare_forge_lint, + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + MIXED_CASE_FUNCTION, + Severity::Info, + "mixed-case-function", + "function names should use mixedCase.", + "https://docs.soliditylang.org/en/latest/style-guide.html#function-names" +); + +impl<'ast> EarlyLintPass<'ast> for MixedCaseFunction { + fn check_item_function(&mut self, ctx: &LintContext<'_>, func: &'ast ItemFunction<'ast>) { + if let Some(name) = func.header.name { + let name = name.as_str(); + if !is_mixed_case(name) && name.len() > 1 { + ctx.emit(&MIXED_CASE_FUNCTION, func.body_span); + } + } + } +} + +declare_forge_lint!( + MIXED_CASE_VARIABLE, + Severity::Info, + "mixed-case-variable", + "mutable variables should use mixedCase" +); + +impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable { + fn check_variable_definition( + &mut self, + ctx: &LintContext<'_>, + var: &'ast VariableDefinition<'ast>, + ) { + if var.mutability.is_none() { + if let Some(name) = var.name { + let name = name.as_str(); + if !is_mixed_case(name) { + ctx.emit(&MIXED_CASE_VARIABLE, var.span); + } + } + } + } +} + +/// Check if a string is mixedCase +/// +/// To avoid false positives like `fn increment()` or `uin256 counter`, +/// lowercase strings are treated as mixedCase. +pub fn is_mixed_case(s: &str) -> bool { + if s.len() <= 1 { + return true; + } + + // Remove leading/trailing underscores like `heck` does + s.trim_matches('_') == format!("{}", heck::AsLowerCamelCase(s)).as_str() +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use super::*; + use crate::{linter::Lint, sol::SolidityLinter}; + + #[test] + fn test_variable_mixed_case() -> eyre::Result<()> { + let linter = SolidityLinter::new() + .with_lints(Some(vec![MIXED_CASE_VARIABLE])) + .with_buffer_emitter(true); + + let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning[{}]", MIXED_CASE_VARIABLE.id())).count(); + let notes = emitted.matches(&format!("note[{}]", MIXED_CASE_VARIABLE.id())).count(); + + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 6, "Expected 6 notes"); + + Ok(()) + } + + #[test] + fn test_function_mixed_case() -> eyre::Result<()> { + let linter = SolidityLinter::new() + .with_lints(Some(vec![MIXED_CASE_FUNCTION])) + .with_buffer_emitter(true); + + let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning[{}]", MIXED_CASE_FUNCTION.id())).count(); + let notes = emitted.matches(&format!("note[{}]", MIXED_CASE_FUNCTION.id())).count(); + + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 3, "Expected 3 notes"); + + Ok(()) + } +} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs new file mode 100644 index 0000000000000..7a57e1afa3351 --- /dev/null +++ b/crates/lint/src/sol/info/mod.rs @@ -0,0 +1,20 @@ +mod mixed_case; +use mixed_case::{MIXED_CASE_FUNCTION, MIXED_CASE_VARIABLE}; + +mod pascal_case; +use pascal_case::PASCAL_CASE_STRUCT; + +mod screaming_snake_case; +use screaming_snake_case::SCREAMING_SNAKE_CASE; + +use crate::{ + register_lints, + sol::{EarlyLintPass, SolLint}, +}; + +register_lints!( + (MixedCaseVariable, MIXED_CASE_VARIABLE), + (ScreamingSnakeCase, SCREAMING_SNAKE_CASE), + (PascalCaseStruct, PASCAL_CASE_STRUCT), + (MixedCaseFunction, MIXED_CASE_FUNCTION) +); diff --git a/crates/lint/src/sol/info/pascal_case.rs b/crates/lint/src/sol/info/pascal_case.rs new file mode 100644 index 0000000000000..b50bbf385dee8 --- /dev/null +++ b/crates/lint/src/sol/info/pascal_case.rs @@ -0,0 +1,59 @@ +use solar_ast::ItemStruct; + +use super::PascalCaseStruct; +use crate::{ + declare_forge_lint, + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + PASCAL_CASE_STRUCT, + Severity::Info, + "struct-pascal-case", + "structs should use PascalCase.", + "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names" +); + +impl<'ast> EarlyLintPass<'ast> for PascalCaseStruct { + fn check_item_struct(&mut self, ctx: &LintContext<'_>, strukt: &'ast ItemStruct<'ast>) { + let name = strukt.name.as_str(); + if !is_pascal_case(name) && name.len() > 1 { + ctx.emit(&PASCAL_CASE_STRUCT, strukt.name.span); + } + } +} + +/// Check if a string is PascalCase +pub fn is_pascal_case(s: &str) -> bool { + if s.len() <= 1 { + return true; + } + + s == format!("{}", heck::AsPascalCase(s)).as_str() +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use super::*; + use crate::{linter::Lint, sol::SolidityLinter}; + + #[test] + fn test_struct_pascal_case() -> eyre::Result<()> { + let linter = SolidityLinter::new() + .with_lints(Some(vec![PASCAL_CASE_STRUCT])) + .with_buffer_emitter(true); + + let emitted = + linter.lint_file(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning[{}]", PASCAL_CASE_STRUCT.id())).count(); + let notes = emitted.matches(&format!("note[{}]", PASCAL_CASE_STRUCT.id())).count(); + + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 6, "Expected 7 notes"); + + Ok(()) + } +} diff --git a/crates/lint/src/sol/info/screaming_snake_case.rs b/crates/lint/src/sol/info/screaming_snake_case.rs new file mode 100644 index 0000000000000..09b997c2690f7 --- /dev/null +++ b/crates/lint/src/sol/info/screaming_snake_case.rs @@ -0,0 +1,70 @@ +use solar_ast::VariableDefinition; + +use super::ScreamingSnakeCase; +use crate::{ + declare_forge_lint, + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + SCREAMING_SNAKE_CASE, + Severity::Info, + "screaming-snake-case", + "constants and immutables should use SCREAMING_SNAKE_CASE", + "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names" +); + +impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { + fn check_variable_definition( + &mut self, + ctx: &LintContext<'_>, + var: &'ast VariableDefinition<'ast>, + ) { + if let Some(mutability) = var.mutability { + if mutability.is_constant() || mutability.is_immutable() { + if let Some(name) = var.name { + let name = name.as_str(); + if !is_screaming_snake_case(name) && name.len() > 1 { + ctx.emit(&SCREAMING_SNAKE_CASE, var.span); + } + } + } + } + } +} + +/// Check if a string is SCREAMING_SNAKE_CASE. Numbers don't need to be preceeded by an underscore. +pub fn is_screaming_snake_case(s: &str) -> bool { + if s.len() <= 1 { + return true; + } + + // Remove leading/trailing underscores like `heck` does + s.trim_matches('_') == format!("{}", heck::AsShoutySnakeCase(s)).as_str() +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use super::*; + use crate::{linter::Lint, sol::SolidityLinter}; + + #[test] + fn test_screaming_snake_case() -> eyre::Result<()> { + let linter = SolidityLinter::new() + .with_lints(Some(vec![SCREAMING_SNAKE_CASE])) + .with_buffer_emitter(true); + + let emitted = + linter.lint_file(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); + let warnings = emitted.matches(&format!("warning[{}]", SCREAMING_SNAKE_CASE.id())).count(); + let notes = emitted.matches(&format!("note[{}]", SCREAMING_SNAKE_CASE.id())).count(); + + assert_eq!(warnings, 0, "Expected 0 warnings"); + assert_eq!(notes, 8, "Expected 8 notes"); + + Ok(()) + } +} diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs new file mode 100644 index 0000000000000..10ccc3ff70ade --- /dev/null +++ b/crates/lint/src/sol/macros.rs @@ -0,0 +1,73 @@ +/// Macro for defining lints and relevant metadata for the Solidity linter. +/// +/// # Parameters +/// +/// Each lint requires the following input fields: +/// - `$id`: Identitifier of the generated [`SolLint`] constant. +/// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). +/// - `$str_id`: A unique identifier used to reference a specific lint during configuration. +/// - `$desc`: A short description of the lint. +/// - `$help` (optional): Link to additional information about the lint or best practices. +#[macro_export] +macro_rules! declare_forge_lint { + ($id:ident, $severity:expr, $str_id:expr, $desc:expr, $help:expr) => { + // Declare the static `Lint` metadata + pub static $id: SolLint = SolLint { + id: $str_id, + severity: $severity, + description: $desc, + help: if $help.is_empty() { None } else { Some($help) }, + }; + }; + + ($id:ident, $severity:expr, $str_id:expr, $desc:expr) => { + $crate::declare_forge_lint!($id, $severity, $str_id, $desc, ""); + }; +} + +/// Registers Solidity linter passes with their corresponding [`SolLint`]. +/// +/// # Parameters +/// +/// - `$pass_id`: Identitifier of the generated struct that will implement the pass trait. +/// - `$lint`: [`SolLint`] constant. +/// +/// # Outputs +/// +/// - Structs for each linting pass (which should manually implement `EarlyLintPass`) +/// - `const REGISTERED_LINTS` containing all registered lint objects +/// - `const LINT_PASSES` mapping each lint to its corresponding pass +#[macro_export] +macro_rules! register_lints { + ($(($pass_id:ident, $lint:expr)),* $(,)?) => { + // Declare the structs that will implement the pass trait + $( + #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] + pub struct $pass_id; + + impl $pass_id { + pub fn as_lint_pass<'a>() -> Box> { + Box::new(Self::default()) + } + } + )* + + // Expose array constants + pub const REGISTERED_LINTS: &[SolLint] = &[$($lint),*]; + pub const LINT_PASSES: &[(SolLint, fn() -> Box>)] = &[ + $( + ($lint, || Box::new($pass_id::default())), + )* + ]; + + // Helper function to create lint passes with the required lifetime + pub fn create_lint_passes<'a>() -> Vec<(Box>, SolLint)> + { + vec![ + $( + ($pass_id::as_lint_pass(), $lint), + )* + ] + } + }; +} diff --git a/crates/lint/src/sol/med.rs b/crates/lint/src/sol/med/div_mul.rs similarity index 76% rename from crates/lint/src/sol/med.rs rename to crates/lint/src/sol/med/div_mul.rs index 30bb97059e13a..e5d275aac7561 100644 --- a/crates/lint/src/sol/med.rs +++ b/crates/lint/src/sol/med/div_mul.rs @@ -1,17 +1,27 @@ use solar_ast::{BinOp, BinOpKind, Expr, ExprKind}; -use std::ops::ControlFlow; -use super::{DivideBeforeMultiply, DIVIDE_BEFORE_MULTIPLY}; -use crate::linter::{EarlyLintPass, LintContext}; +use super::DivideBeforeMultiply; +use crate::{ + declare_forge_lint, + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + DIVIDE_BEFORE_MULTIPLY, + Severity::Med, + "divide-before-multiply", + "multiplication should occur before division to avoid loss of precision", + "" +); impl<'ast> EarlyLintPass<'ast> for DivideBeforeMultiply { - fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) -> ControlFlow<()> { + fn check_expr(&mut self, ctx: &LintContext<'_>, expr: &'ast Expr<'ast>) { if let ExprKind::Binary(left_expr, BinOp { kind: BinOpKind::Mul, .. }, _) = &expr.kind { if contains_division(left_expr) { ctx.emit(&DIVIDE_BEFORE_MULTIPLY, expr.span); } } - ControlFlow::Continue(()) } } @@ -45,8 +55,8 @@ mod test { let emitted = linter.lint_file(Path::new("testdata/DivideBeforeMultiply.sol")).unwrap().to_string(); let warnings = - emitted.matches(&format!("warning: {}", DIVIDE_BEFORE_MULTIPLY.id())).count(); - let notes = emitted.matches(&format!("note: {}", DIVIDE_BEFORE_MULTIPLY.id())).count(); + emitted.matches(&format!("warning[{}]", DIVIDE_BEFORE_MULTIPLY.id())).count(); + let notes = emitted.matches(&format!("note[{}]", DIVIDE_BEFORE_MULTIPLY.id())).count(); assert_eq!(warnings, 6, "Expected 6 warnings"); assert_eq!(notes, 0, "Expected 0 notes"); diff --git a/crates/lint/src/sol/med/mod.rs b/crates/lint/src/sol/med/mod.rs new file mode 100644 index 0000000000000..7b0888575bd6d --- /dev/null +++ b/crates/lint/src/sol/med/mod.rs @@ -0,0 +1,9 @@ +mod div_mul; +use div_mul::DIVIDE_BEFORE_MULTIPLY; + +use crate::{ + register_lints, + sol::{EarlyLintPass, SolLint}, +}; + +register_lints!((DivideBeforeMultiply, DIVIDE_BEFORE_MULTIPLY)); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index da17de51af592..4339d4bf10600 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,3 +1,5 @@ +pub mod macros; + pub mod gas; pub mod high; pub mod info; @@ -77,15 +79,11 @@ impl SolidityLinter { let _ = sess.enter(|| -> Result<(), ErrorGuaranteed> { // Declare all available passes and lints - let passes_and_lints: Vec<(Box>, SolLint)> = vec![ - (Box::new(AsmKeccak256), ASM_KECCACK256), - (Box::new(IncorrectShift), INCORRECT_SHIFT), - (Box::new(DivideBeforeMultiply), DIVIDE_BEFORE_MULTIPLY), - (Box::new(VariableMixedCase), VARIABLE_MIXED_CASE), - (Box::new(ScreamingSnakeCase), SCREAMING_SNAKE_CASE), - (Box::new(StructPascalCase), STRUCT_PASCAL_CASE), - (Box::new(FunctionMixedCase), FUNCTION_MIXED_CASE), - ]; + let mut passes_and_lints = Vec::new(); + passes_and_lints.extend(gas::create_lint_passes()); + passes_and_lints.extend(high::create_lint_passes()); + passes_and_lints.extend(med::create_lint_passes()); + passes_and_lints.extend(info::create_lint_passes()); // Filter based on linter config let mut passes: Vec>> = passes_and_lints @@ -168,119 +166,34 @@ impl Lint for SolLint { } } -macro_rules! declare_forge_lints { - ($(($pass_id:ident, $lint_id:ident, $severity:expr, $str_id:expr, $description:expr, $help:expr)),* $(,)?) => { - // Declare the static `Lint` metadata - $( - pub static $lint_id: SolLint = SolLint { - id: $str_id, - severity: $severity, - description: $description, - help: if $help.is_empty() { None } else { Some($help) } - }; - )* +impl<'a> TryFrom<&'a str> for SolLint { + type Error = SolLintError; - // Implement TryFrom<&str> for `SolLint` - impl<'a> TryFrom<&'a str> for SolLint { - type Error = SolLintError; + fn try_from(value: &'a str) -> Result { + for &lint in high::REGISTERED_LINTS { + if lint.id() == value { + return Ok(lint); + } + } - fn try_from(value: &'a str) -> Result { - match value { - $( - $str_id => Ok($lint_id), - )* - _ => Err(SolLintError::InvalidId(value.to_string())), - } + for &lint in med::REGISTERED_LINTS { + if lint.id() == value { + return Ok(lint); } } - // Declare the structs that will implement the pass trait - $( - #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] - pub struct $pass_id; - )* - }; -} + for &lint in info::REGISTERED_LINTS { + if lint.id() == value { + return Ok(lint); + } + } -// Macro for defining lints and relevant metadata for the Solidity linter. -// -// This macro generates the [`SolLint`] enum with each lint along with utility methods and -// corresponding structs for each lint specified. -// -// # Parameters -// -// Each lint is defined as a tuple with the following fields: -// - `$id`: Identitifier used as the struct and enum variant created for the lint. -// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). -// - `$description`: A short description of the lint. -// - `$help`: Link to additional information about the lint or best practices. -// - `$str_id`: A unique identifier used to reference a specific lint during configuration. -declare_forge_lints!( - //High - ( - IncorrectShift, - INCORRECT_SHIFT, - Severity::High, - "incorrect-shift", - "The order of args in a shift operation is incorrect", - "" - ), - // Med - ( - DivideBeforeMultiply, - DIVIDE_BEFORE_MULTIPLY, - Severity::Med, - "divide-before-multiply", - "Multiplication should occur before division to avoid loss of precision", - "" - ), - // Low + for &lint in gas::REGISTERED_LINTS { + if lint.id() == value { + return Ok(lint); + } + } - // Info - ( - VariableMixedCase, - VARIABLE_MIXED_CASE, - Severity::Info, - "variable-mixed-case", - "Mutable variables should use mixedCase", - "" - ), - ( - ScreamingSnakeCase, - SCREAMING_SNAKE_CASE, - Severity::Info, - "screaming-snake-case", - "Constants and immutables should use SCREAMING_SNAKE_CASE", - "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names" - ), - ( - StructPascalCase, - STRUCT_PASCAL_CASE, - Severity::Info, - "struct-pascal-case", - "Structs should use PascalCase.", - "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names" - ), - ( - FunctionMixedCase, - FUNCTION_MIXED_CASE, - Severity::Info, - "function-mixed-case", - "Function names should use mixedCase.", - "https://docs.soliditylang.org/en/latest/style-guide.html#function-names" - ), - // Gas Optimizations - ( - AsmKeccak256, - ASM_KECCACK256, - Severity::Gas, - "asm-keccack256", - "Hash via inline assembly to save gas", - "" - ), - // TODO: PackStorageVariables - // TODO: PackStructs - // TODO: UseConstantVariable - // TODO: UseImmutableVariable - // TODO: UseCalldataInsteadOfMemory -); + Err(SolLintError::InvalidId(value.to_string())) + } +} diff --git a/crates/lint/testdata/MixedCase.sol b/crates/lint/testdata/MixedCase.sol index ebe71c159fce4..7cd68f0a5862e 100644 --- a/crates/lint/testdata/MixedCase.sol +++ b/crates/lint/testdata/MixedCase.sol @@ -15,10 +15,10 @@ contract MixedCaseTest { function foo() public { // Passes uint256 testVal; - uint256 testVAL; uint256 testVal123; // Fails + uint256 testVAL; uint256 TestVal; uint256 TESTVAL; } @@ -26,10 +26,10 @@ contract MixedCaseTest { // Passes function functionMixedCase() public {} function _functionMixedCase() internal {} - function functionmixedcase() public {} + function functionmixedcase() public {} // Fails function Functionmixedcase() public {} - function FUNCTION_MIXED_CASE() public {} - function FunctionMixedCase() public {} + function FUNCTION_MIXED_CASE() public {} + function FunctionMixedCase() public {} } diff --git a/crates/lint/testdata/ScreamingSnakeCase.sol b/crates/lint/testdata/ScreamingSnakeCase.sol index a3cb29532d154..2403fa4b52fb2 100644 --- a/crates/lint/testdata/ScreamingSnakeCase.sol +++ b/crates/lint/testdata/ScreamingSnakeCase.sol @@ -8,13 +8,14 @@ contract ScreamingSnakeCaseTest { uint256 immutable _SCREAMING_SNAKE_CASE_1 = 0; uint256 immutable SCREAMING_SNAKE_CASE_1 = 0; uint256 constant SCREAMINGSNAKECASE = 0; + uint256 immutable SCREAMINGSNAKECASE0 = 0; + uint256 immutable SCREAMINGSNAKECASE_ = 0; // Fails uint256 constant screamingSnakeCase = 0; uint256 constant screaming_snake_case = 0; uint256 constant ScreamingSnakeCase = 0; uint256 constant SCREAMING_snake_case = 0; - uint256 immutable SCREAMINGSNAKECASE0 = 0; uint256 immutable screamingSnakeCase0 = 0; uint256 immutable screaming_snake_case0 = 0; uint256 immutable ScreamingSnakeCase0 = 0; diff --git a/crates/lint/testdata/StructPascalCase.sol b/crates/lint/testdata/StructPascalCase.sol index a79758f24baf9..d7322c1e7e17c 100644 --- a/crates/lint/testdata/StructPascalCase.sol +++ b/crates/lint/testdata/StructPascalCase.sol @@ -7,6 +7,10 @@ contract StructPascalCaseTest { uint256 a; } + struct PascalCAse { + uint256 a; + } + // Fails struct _PascalCase { uint256 a; @@ -31,8 +35,4 @@ contract StructPascalCaseTest { struct PASCALCASE { uint256 a; } - - struct PascalCAse { - uint256 a; - } } From 3a07e320bca699bda665bf21360efa02cf686fa6 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 5 May 2025 10:49:27 +0200 Subject: [PATCH 086/107] fix: single session with parallel linting per file --- crates/forge/src/cmd/lint.rs | 1 - crates/lint/src/sol/gas/keccack.rs | 5 +- crates/lint/src/sol/high/incorrect_shift.rs | 5 +- crates/lint/src/sol/info/mixed_case.rs | 12 ++-- crates/lint/src/sol/info/pascal_case.rs | 6 +- .../lint/src/sol/info/screaming_snake_case.rs | 8 +-- crates/lint/src/sol/macros.rs | 6 +- crates/lint/src/sol/med/div_mul.rs | 6 +- crates/lint/src/sol/mod.rs | 60 +++++++++---------- 9 files changed, 45 insertions(+), 64 deletions(-) diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index d41f0751aa55b..6ef6a2d68df72 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -110,7 +110,6 @@ impl LintArgs { if project.compiler.solc.is_some() { let linter = SolidityLinter::new() - .with_description(true) .with_lints(if include.is_empty() { None } else { Some(include) }) .without_lints(if exclude.is_empty() { None } else { Some(exclude) }) .with_severity(if severity.is_empty() { None } else { Some(severity) }); diff --git a/crates/lint/src/sol/gas/keccack.rs b/crates/lint/src/sol/gas/keccack.rs index c3259fd81c8ff..144f8fac6f2cd 100644 --- a/crates/lint/src/sol/gas/keccack.rs +++ b/crates/lint/src/sol/gas/keccack.rs @@ -37,10 +37,9 @@ mod test { #[test] fn test_keccak256() -> eyre::Result<()> { - let linter = - SolidityLinter::new().with_lints(Some(vec![ASM_KECCACK256])).with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![ASM_KECCACK256])); - let emitted = linter.lint_file(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); + let emitted = linter.lint_test(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", ASM_KECCACK256.id())).count(); let notes = emitted.matches(&format!("note[{}]", ASM_KECCACK256.id())).count(); diff --git a/crates/lint/src/sol/high/incorrect_shift.rs b/crates/lint/src/sol/high/incorrect_shift.rs index f680630c5e49d..e43cc8f3d019b 100644 --- a/crates/lint/src/sol/high/incorrect_shift.rs +++ b/crates/lint/src/sol/high/incorrect_shift.rs @@ -52,11 +52,10 @@ mod test { #[test] fn test_incorrect_shift() -> eyre::Result<()> { - let linter = - SolidityLinter::new().with_lints(Some(vec![INCORRECT_SHIFT])).with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![INCORRECT_SHIFT])); let emitted = - linter.lint_file(Path::new("testdata/IncorrectShift.sol")).unwrap().to_string(); + linter.lint_test(Path::new("testdata/IncorrectShift.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", INCORRECT_SHIFT.id())).count(); let notes = emitted.matches(&format!("note[{}]", INCORRECT_SHIFT.id())).count(); diff --git a/crates/lint/src/sol/info/mixed_case.rs b/crates/lint/src/sol/info/mixed_case.rs index cf076fe3b7a08..d91b57ab5ea3b 100644 --- a/crates/lint/src/sol/info/mixed_case.rs +++ b/crates/lint/src/sol/info/mixed_case.rs @@ -72,11 +72,9 @@ mod test { #[test] fn test_variable_mixed_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![MIXED_CASE_VARIABLE])) - .with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![MIXED_CASE_VARIABLE])); - let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let emitted = linter.lint_test(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", MIXED_CASE_VARIABLE.id())).count(); let notes = emitted.matches(&format!("note[{}]", MIXED_CASE_VARIABLE.id())).count(); @@ -88,11 +86,9 @@ mod test { #[test] fn test_function_mixed_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![MIXED_CASE_FUNCTION])) - .with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![MIXED_CASE_FUNCTION])); - let emitted = linter.lint_file(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); + let emitted = linter.lint_test(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", MIXED_CASE_FUNCTION.id())).count(); let notes = emitted.matches(&format!("note[{}]", MIXED_CASE_FUNCTION.id())).count(); diff --git a/crates/lint/src/sol/info/pascal_case.rs b/crates/lint/src/sol/info/pascal_case.rs index b50bbf385dee8..4245e2e322636 100644 --- a/crates/lint/src/sol/info/pascal_case.rs +++ b/crates/lint/src/sol/info/pascal_case.rs @@ -42,12 +42,10 @@ mod test { #[test] fn test_struct_pascal_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![PASCAL_CASE_STRUCT])) - .with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![PASCAL_CASE_STRUCT])); let emitted = - linter.lint_file(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); + linter.lint_test(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", PASCAL_CASE_STRUCT.id())).count(); let notes = emitted.matches(&format!("note[{}]", PASCAL_CASE_STRUCT.id())).count(); diff --git a/crates/lint/src/sol/info/screaming_snake_case.rs b/crates/lint/src/sol/info/screaming_snake_case.rs index 09b997c2690f7..cb2370563753c 100644 --- a/crates/lint/src/sol/info/screaming_snake_case.rs +++ b/crates/lint/src/sol/info/screaming_snake_case.rs @@ -34,7 +34,7 @@ impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { } } -/// Check if a string is SCREAMING_SNAKE_CASE. Numbers don't need to be preceeded by an underscore. +/// Check if a string is SCREAMING_SNAKE_CASE. Numbers don't need to be preceded by an underscore. pub fn is_screaming_snake_case(s: &str) -> bool { if s.len() <= 1 { return true; @@ -53,12 +53,10 @@ mod test { #[test] fn test_screaming_snake_case() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![SCREAMING_SNAKE_CASE])) - .with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![SCREAMING_SNAKE_CASE])); let emitted = - linter.lint_file(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); + linter.lint_test(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", SCREAMING_SNAKE_CASE.id())).count(); let notes = emitted.matches(&format!("note[{}]", SCREAMING_SNAKE_CASE.id())).count(); diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index 10ccc3ff70ade..727e4aa35ce31 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -3,8 +3,8 @@ /// # Parameters /// /// Each lint requires the following input fields: -/// - `$id`: Identitifier of the generated [`SolLint`] constant. -/// - `$severity`: The [`Severity`] of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). +/// - `$id`: Identitifier of the generated `SolLint` constant. +/// - `$severity`: The `Severity` of the lint (e.g. `High`, `Med`, `Low`, `Info`, `Gas`). /// - `$str_id`: A unique identifier used to reference a specific lint during configuration. /// - `$desc`: A short description of the lint. /// - `$help` (optional): Link to additional information about the lint or best practices. @@ -30,7 +30,7 @@ macro_rules! declare_forge_lint { /// # Parameters /// /// - `$pass_id`: Identitifier of the generated struct that will implement the pass trait. -/// - `$lint`: [`SolLint`] constant. +/// - `$lint`: `SolLint` constant. /// /// # Outputs /// diff --git a/crates/lint/src/sol/med/div_mul.rs b/crates/lint/src/sol/med/div_mul.rs index e5d275aac7561..6c6b16927c7a0 100644 --- a/crates/lint/src/sol/med/div_mul.rs +++ b/crates/lint/src/sol/med/div_mul.rs @@ -48,12 +48,10 @@ mod test { #[test] fn test_divide_before_multiply() -> eyre::Result<()> { - let linter = SolidityLinter::new() - .with_lints(Some(vec![DIVIDE_BEFORE_MULTIPLY])) - .with_buffer_emitter(true); + let linter = SolidityLinter::new().with_lints(Some(vec![DIVIDE_BEFORE_MULTIPLY])); let emitted = - linter.lint_file(Path::new("testdata/DivideBeforeMultiply.sol")).unwrap().to_string(); + linter.lint_test(Path::new("testdata/DivideBeforeMultiply.sol")).unwrap().to_string(); let warnings = emitted.matches(&format!("warning[{}]", DIVIDE_BEFORE_MULTIPLY.id())).count(); let notes = emitted.matches(&format!("note[{}]", DIVIDE_BEFORE_MULTIPLY.id())).count(); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 4339d4bf10600..abd052fe86445 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -6,14 +6,12 @@ pub mod info; pub mod med; use crate::linter::{EarlyLintPass, EarlyLintVisitor, Lint, LintContext, Linter}; + use foundry_compilers::solc::SolcLanguage; use foundry_config::lint::Severity; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena}; -use solar_interface::{ - diagnostics::{EmittedDiagnostics, ErrorGuaranteed}, - ColorChoice, Session, -}; +use solar_interface::{diagnostics, Session}; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -25,19 +23,11 @@ pub struct SolidityLinter { lints_included: Option>, lints_excluded: Option>, with_description: bool, - // This field is only used for testing purposes, in production it will always be false. - with_buffer_emitter: bool, } impl SolidityLinter { pub fn new() -> Self { - Self { - severity: None, - lints_included: None, - lints_excluded: None, - with_description: false, - with_buffer_emitter: false, - } + Self { severity: None, lints_included: None, lints_excluded: None, with_description: true } } pub fn with_severity(mut self, severity: Option>) -> Self { @@ -61,23 +51,22 @@ impl SolidityLinter { } #[cfg(test)] - pub(crate) fn with_buffer_emitter(mut self, with: bool) -> Self { - self.with_buffer_emitter = with; - self - } - - // Helper function to ease testing, despite `fn lint` being the public API for the `Linter` - pub(crate) fn lint_file(&self, file: &Path) -> Option { - let mut sess = if self.with_buffer_emitter { - Session::builder().with_buffer_emitter(ColorChoice::Never).build() - } else { - Session::builder().with_stderr_emitter().build() - }; + /// Helper function to ease testing, despite `fn lint` being the public API for the `Linter`. + /// Logs the diagnostics to the local buffer, so that tests can perform assertions. + pub(crate) fn lint_test(&self, file: &Path) -> Option { + let mut sess = + Session::builder().with_buffer_emitter(solar_interface::ColorChoice::Never).build(); sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); + self.process_file(&sess, file); + + sess.emitted_diagnostics() + } + + fn process_file(&self, sess: &Session, file: &Path) { let arena = Arena::new(); - let _ = sess.enter(|| -> Result<(), ErrorGuaranteed> { + let _ = sess.enter(|| -> Result<(), diagnostics::ErrorGuaranteed> { // Declare all available passes and lints let mut passes_and_lints = Vec::new(); passes_and_lints.extend(gas::create_lint_passes()); @@ -111,18 +100,16 @@ impl SolidityLinter { .collect(); // Initialize the parser and get the AST - let mut parser = solar_parse::Parser::from_file(&sess, &arena, file)?; + let mut parser = solar_parse::Parser::from_file(sess, &arena, file)?; let ast = parser.parse_file().map_err(|e| e.emit())?; // Initialize and run the visitor - let ctx = LintContext::new(&sess, self.with_description); + let ctx = LintContext::new(sess, self.with_description); let mut visitor = EarlyLintVisitor { ctx: &ctx, passes: &mut passes }; - let _ = visitor.visit_source_unit(&ast); + _ = visitor.visit_source_unit(&ast); Ok(()) }); - - sess.emitted_diagnostics() } } @@ -131,8 +118,15 @@ impl Linter for SolidityLinter { type Lint = SolLint; fn lint(&self, input: &[PathBuf]) { - input.into_par_iter().for_each(|file| { - _ = self.lint_file(file); + // Create a single session for all files + let mut sess = Session::builder().with_stderr_emitter().build(); + sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); + + // Process the files in parallel + sess.enter_parallel(|| { + input.into_par_iter().for_each(|file| { + self.process_file(&sess, file); + }); }); } } From 21d3d61b684d68c411f4037d6adc58b72f51a943 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 5 May 2025 11:27:21 +0200 Subject: [PATCH 087/107] style: fix docs errors + typos --- crates/config/src/lint.rs | 5 ----- crates/forge/src/cmd/lint.rs | 24 +++++++----------------- crates/forge/tests/cli/lint.rs | 1 - crates/lint/src/sol/macros.rs | 2 +- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/crates/config/src/lint.rs b/crates/config/src/lint.rs index b5dc7dc315d93..3fe3b44cdfca7 100644 --- a/crates/config/src/lint.rs +++ b/crates/config/src/lint.rs @@ -15,11 +15,6 @@ pub struct LinterConfig { /// If uninformed, all severities are checked. pub severity: Vec, - /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). - /// - /// Cannot be used in combination with `exclude_lint`. - pub include_lints: Vec, - /// Deny specific lints based on their ID (e.g. "mixed-case-function"). /// /// Cannot be used in combination with `include_lint`. diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index 6ef6a2d68df72..be81206f953c2 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -7,7 +7,7 @@ use forge_lint::{ use foundry_cli::utils::{FoundryPathExt, LoadConfig}; use foundry_compilers::{solc::SolcLanguage, utils::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic, lint::Severity}; -use std::{collections::HashSet, path::PathBuf}; +use std::path::PathBuf; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] @@ -86,20 +86,9 @@ impl LintArgs { }; // Override default lint config with user-defined lints - let (include, exclude) = if let Some(cli_lints) = &self.lint { - let include_lints = parse_lints(cli_lints)?; - let target_ids: HashSet<&str> = cli_lints.iter().map(String::as_str).collect(); - let filtered_excludes = config - .lint - .exclude_lints - .iter() - .filter(|l| !target_ids.contains(l.as_str())) - .cloned() - .collect::>(); - - (include_lints, parse_lints(&filtered_excludes)?) - } else { - (parse_lints(&config.lint.include_lints)?, parse_lints(&config.lint.exclude_lints)?) + let (include, exclude) = match &self.lint { + Some(cli_lints) => (Some(parse_lints(cli_lints)?), None), + None => (None, Some(parse_lints(&config.lint.exclude_lints)?)), }; // Override default severity config with user-defined severity @@ -110,8 +99,9 @@ impl LintArgs { if project.compiler.solc.is_some() { let linter = SolidityLinter::new() - .with_lints(if include.is_empty() { None } else { Some(include) }) - .without_lints(if exclude.is_empty() { None } else { Some(exclude) }) + .with_description(true) + .with_lints(include) + .without_lints(exclude) .with_severity(if severity.is_empty() { None } else { Some(severity) }); linter.lint(&input); diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index 1e80dff53ff72..10ecbf1ff18c8 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -124,7 +124,6 @@ forgetest!(can_override_config_path, |prj, cmd| { severity: vec![LintSeverity::High, LintSeverity::Med], exclude_lints: vec!["incorrect-shift".into()], ignore: vec!["src/ContractWithLints.sol".into()], - ..Default::default() }, ..Default::default() }); diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index 727e4aa35ce31..2da25bd69073d 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -25,7 +25,7 @@ macro_rules! declare_forge_lint { }; } -/// Registers Solidity linter passes with their corresponding [`SolLint`]. +/// Registers Solidity linter passes with their corresponding `SolLint`. /// /// # Parameters /// From d8dbd2d70317b94f8b230a114087965607ba074c Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 5 May 2025 11:41:32 +0200 Subject: [PATCH 088/107] docs: ref to deleted field --- crates/config/src/lint.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/config/src/lint.rs b/crates/config/src/lint.rs index 3fe3b44cdfca7..10d86c96fb321 100644 --- a/crates/config/src/lint.rs +++ b/crates/config/src/lint.rs @@ -16,8 +16,6 @@ pub struct LinterConfig { pub severity: Vec, /// Deny specific lints based on their ID (e.g. "mixed-case-function"). - /// - /// Cannot be used in combination with `include_lint`. pub exclude_lints: Vec, /// Globs to ignore From 043e5350ec17bbf43044574c5f6f7c795ddec201 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 5 May 2025 12:54:05 +0200 Subject: [PATCH 089/107] fix: preprocessor regression + cargo.toml + default lint config tests --- Cargo.lock | 203 ++++--------------------- Cargo.toml | 20 +-- crates/common/src/preprocessor/deps.rs | 3 - crates/forge/src/cmd/lint.rs | 6 +- crates/forge/tests/cli/config.rs | 2 - crates/forge/tests/cli/lint.rs | 48 +++--- 6 files changed, 66 insertions(+), 216 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42315cb0b4f4a..2ee7b2dd28cf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2250,7 +2250,7 @@ dependencies = [ "serde", "serde_json", "solang-parser", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse", "strum 0.27.1", "tikv-jemallocator", "time", @@ -3571,8 +3571,8 @@ dependencies = [ "similar", "similar-asserts", "solang-parser", - "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-interface", + "solar-parse", "soldeer-commands", "strum 0.27.1", "svm-rs", @@ -3643,9 +3643,9 @@ dependencies = [ "rayon", "serde", "serde_json", - "solar-ast 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-ast", + "solar-interface", + "solar-parse", "thiserror 2.0.12", "yansi", ] @@ -3928,8 +3928,8 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-sema 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse", + "solar-sema", "terminal_size", "thiserror 2.0.12", "tokio", @@ -3986,8 +3986,8 @@ dependencies = [ "serde", "serde_json", "sha2", - "solar-parse 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-sema 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "solar-parse", + "solar-sema", "svm-rs", "svm-rs-builds", "tempfile", @@ -4096,8 +4096,8 @@ dependencies = [ "serde", "serde_json", "similar-asserts", - "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-interface", + "solar-parse", "soldeer-core", "tempfile", "thiserror 2.0.12", @@ -4260,7 +4260,7 @@ dependencies = [ "revm-inspectors", "serde", "serde_json", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-parse", "tempfile", "tokio", "tracing", @@ -7807,15 +7807,6 @@ dependencies = [ "regex", ] -[[package]] -name = "scc" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.27" @@ -7873,12 +7864,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "sdd" -version = "3.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" - [[package]] name = "sec1" version = "0.7.3" @@ -8344,26 +8329,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "solar-ast" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a583a12e73099d1f54bfe7c8a30d7af5ff3591c61ee51cce91045ee5496d86" -dependencies = [ - "alloy-primitives 0.8.25", - "bumpalo", - "derive_more 2.0.1", - "either", - "num-bigint", - "num-rational", - "semver 1.0.26", - "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-interface 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "strum 0.27.1", - "typed-arena", -] - [[package]] name = "solar-ast" version = "0.1.2" @@ -8375,22 +8340,13 @@ dependencies = [ "num-bigint", "num-rational", "semver 1.0.26", - "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-macros 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-data-structures", + "solar-interface", + "solar-macros", "strum 0.27.1", "typed-arena", ] -[[package]] -name = "solar-config" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12642e7e8490d6855a345b5b9d5e55630bd30f54450a909e28f1385b448baada" -dependencies = [ - "strum 0.27.1", -] - [[package]] name = "solar-config" version = "0.1.2" @@ -8399,21 +8355,6 @@ dependencies = [ "strum 0.27.1", ] -[[package]] -name = "solar-data-structures" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae8902cc28af53e2ba97c450aff7c59d112a433f9ef98fae808e02e25e6dee6" -dependencies = [ - "bumpalo", - "index_vec", - "indexmap 2.9.0", - "parking_lot", - "rayon", - "rustc-hash 2.1.1", - "smallvec", -] - [[package]] name = "solar-data-structures" version = "0.1.2" @@ -8428,35 +8369,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "solar-interface" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded5ec7a5cee351c7a428842d273470180cab259c46f52d502ec3ab5484d0c3a" -dependencies = [ - "annotate-snippets", - "anstream", - "anstyle", - "const-hex", - "derive_builder", - "derive_more 2.0.1", - "dunce", - "itertools 0.10.5", - "itoa", - "lasso", - "match_cfg", - "normalize-path", - "rayon", - "scc", - "scoped-tls", - "solar-config 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 1.0.69", - "tracing", - "unicode-width 0.2.0", -] - [[package]] name = "solar-interface" version = "0.1.2" @@ -8476,25 +8388,14 @@ dependencies = [ "normalize-path", "rayon", "scoped-tls", - "solar-config 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-macros 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-config", + "solar-data-structures", + "solar-macros", "thiserror 1.0.69", "tracing", "unicode-width 0.2.0", ] -[[package]] -name = "solar-macros" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2c9ff6e00eeeff12eac9d589f1f20413d3b71b9c0c292d1eefbd34787e0836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "solar-macros" version = "0.1.2" @@ -8505,27 +8406,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "solar-parse" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1bc1d0253b0f7f2c7cd25ed7bc5d5e8cac43e717d002398250e0e66e43278b" -dependencies = [ - "alloy-primitives 0.8.25", - "bitflags 2.9.0", - "bumpalo", - "itertools 0.10.5", - "memchr", - "num-bigint", - "num-rational", - "num-traits", - "smallvec", - "solar-ast 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-interface 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tracing", -] - [[package]] name = "solar-parse" version = "0.1.2" @@ -8540,41 +8420,12 @@ dependencies = [ "num-rational", "num-traits", "smallvec", - "solar-ast 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-ast", + "solar-data-structures", + "solar-interface", "tracing", ] -[[package]] -name = "solar-sema" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded4b26fb85a0ae2f3277377236af0884c82f38965a2c51046a53016c8b5f332" -dependencies = [ - "alloy-json-abi 0.8.25", - "alloy-primitives 0.8.25", - "bitflags 2.9.0", - "bumpalo", - "derive_more 2.0.1", - "either", - "once_map", - "paste", - "rayon", - "scc", - "serde", - "serde_json", - "solar-ast 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-data-structures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-interface 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-macros 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "solar-parse 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "strum 0.27.1", - "thread_local", - "tracing", - "typed-arena", -] - [[package]] name = "solar-sema" version = "0.1.2" @@ -8591,11 +8442,11 @@ dependencies = [ "rayon", "serde", "serde_json", - "solar-ast 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-data-structures 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-interface 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-macros 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", - "solar-parse 0.1.2 (git+https://github.com/paradigmxyz/solar?branch=main)", + "solar-ast", + "solar-data-structures", + "solar-interface", + "solar-macros", + "solar-parse", "strum 0.27.1", "thread_local", "tracing", diff --git a/Cargo.toml b/Cargo.toml index fe35b05651ce5..541ae5a311019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,15 +194,10 @@ foundry-block-explorers = { version = "0.13.0", default-features = false } foundry-compilers = { version = "0.14.0", default-features = false } foundry-fork-db = "0.12" solang-parser = "=0.3.3" -# TODO: enable again after 0.1.3 release -# solar-ast = { version = "=0.1.2", default-features = false } -# solar-parse = { version = "=0.1.2", default-features = false } -# solar-interface = { version = "=0.1.2", default-features = false } -# solar-sema = { version = "=0.1.2", default-features = false } -solar-ast = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-ast" } -solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-parse" } -solar-interface = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-interface" } -solar-sema = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-sema" } +solar-ast = { version = "=0.1.2", default-features = false } +solar-parse = { version = "=0.1.2", default-features = false } +solar-interface = { version = "=0.1.2", default-features = false } +solar-sema = { version = "=0.1.2", default-features = false } ## revm revm = { version = "19.4.0", default-features = false } @@ -369,3 +364,10 @@ idna_adapter = "=1.1.0" # alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } # alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } # alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } + +# TODO: comment out after 0.1.3 release +# solar +solar-ast = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-ast" } +solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-parse" } +solar-interface = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-interface" } +solar-sema = { git = "https://github.com/paradigmxyz/solar", branch = "main", package = "solar-sema" } diff --git a/crates/common/src/preprocessor/deps.rs b/crates/common/src/preprocessor/deps.rs index dbcfd1b762f33..22feb0d16a468 100644 --- a/crates/common/src/preprocessor/deps.rs +++ b/crates/common/src/preprocessor/deps.rs @@ -323,9 +323,6 @@ pub(crate) fn remove_bytecode_dependencies( "_args: encodeArgs{id}(DeployHelper{id}.FoundryPpConstructorArgs", id = dep.referenced_contract.get() )); - if *call_args_offset > 0 { - update.push('('); - } updates.insert((dep.loc.start, dep.loc.end + call_args_offset, update)); updates.insert(( dep.loc.end + args_length, diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index be81206f953c2..a33e65de74f7a 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -12,7 +12,7 @@ use std::path::PathBuf; /// CLI arguments for `forge lint`. #[derive(Clone, Debug, Parser)] pub struct LintArgs { - /// Path to the file. + /// 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, @@ -23,14 +23,14 @@ pub struct LintArgs { #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")] root: Option, - /// Specifies which lints to run based on severity. Overrides the project config. + /// 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>, /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). Overrides the - /// project config. + /// `exclude_lints` project config. #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))] lint: Option>, } diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index bb911ca99652c..d53210ee95b61 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -1069,7 +1069,6 @@ sort_imports = false [lint] severity = [] -include_lints = [] exclude_lints = [] ignore = [] @@ -1274,7 +1273,6 @@ exclude = [] }, "lint": { "severity": [], - "include_lints": [], "exclude_lints": [], "ignore": [] }, diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index 10ecbf1ff18c8..4250aaa3307d4 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -1,4 +1,4 @@ -use foundry_config::{Config, LintSeverity, LinterConfig}; +use foundry_config::{LintSeverity, LinterConfig}; const CONTRACT: &str = r#" // SPDX-License-Identifier: MIT @@ -39,13 +39,12 @@ forgetest!(can_use_config, |prj, cmd| { prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); // Check config for `severity` and `exclude` - prj.write_config(Config { - lint: LinterConfig { + prj.update_config(|config| { + config.lint = LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], exclude_lints: vec!["incorrect-shift".into()], ..Default::default() - }, - ..Default::default() + }; }); cmd.arg("lint").assert_success().stderr_eq(str![[r#" warning[divide-before-multiply] @@ -66,12 +65,9 @@ forgetest!(can_use_config_ignore, |prj, cmd| { prj.add_source("OtherContract", OTHER_CONTRACT).unwrap(); // Check config for `ignore` - prj.write_config(Config { - lint: LinterConfig { - ignore: vec!["src/ContractWithLints.sol".into()], - ..Default::default() - }, - ..Default::default() + prj.update_config(|config| { + config.lint = + LinterConfig { ignore: vec!["src/ContractWithLints.sol".into()], ..Default::default() }; }); cmd.arg("lint").assert_success().stderr_eq(str![[r#" note[mixed-case-variable] @@ -84,6 +80,15 @@ note[mixed-case-variable] "#]]); + + // Check config again, ignoring all files + prj.update_config(|config| { + config.lint = LinterConfig { + ignore: vec!["src/ContractWithLints.sol".into(), "src/OtherContract.sol".into()], + ..Default::default() + }; + }); + cmd.arg("lint").assert_success().stderr_eq(str![[""]]); }); forgetest!(can_override_config_severity, |prj, cmd| { @@ -92,13 +97,12 @@ forgetest!(can_override_config_severity, |prj, cmd| { prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); // Override severity - prj.write_config(Config { - lint: LinterConfig { + prj.update_config(|config| { + config.lint = LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], ignore: vec!["src/ContractWithLints.sol".into()], ..Default::default() - }, - ..Default::default() + }; }); cmd.arg("lint").args(["--severity", "info"]).assert_success().stderr_eq(str![[r#" note[mixed-case-variable] @@ -119,13 +123,12 @@ forgetest!(can_override_config_path, |prj, cmd| { prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); // Override excluded files - prj.write_config(Config { - lint: LinterConfig { + prj.update_config(|config| { + config.lint = LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], exclude_lints: vec!["incorrect-shift".into()], ignore: vec!["src/ContractWithLints.sol".into()], - }, - ..Default::default() + }; }); cmd.arg("lint").arg("src/ContractWithLints.sol").assert_success().stderr_eq(str![[r#" warning[divide-before-multiply] @@ -146,13 +149,12 @@ forgetest!(can_override_config_lint, |prj, cmd| { prj.add_source("OtherContractWithLints", OTHER_CONTRACT).unwrap(); // Override excluded lints - prj.write_config(Config { - lint: LinterConfig { + prj.update_config(|config| { + config.lint = LinterConfig { severity: vec![LintSeverity::High, LintSeverity::Med], exclude_lints: vec!["incorrect-shift".into()], ..Default::default() - }, - ..Default::default() + }; }); cmd.arg("lint").args(["--only-lint", "incorrect-shift"]).assert_success().stderr_eq(str![[ r#" From e4b0a08f820bb0a7b54ea7a4e751d2a5985c17f5 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Thu, 8 May 2025 10:08:04 +0200 Subject: [PATCH 090/107] test(forge): solar test runner (#2) --- Cargo.lock | 165 ++++++++++++++++-- crates/forge/src/cmd/lint.rs | 5 + crates/forge/tests/cli/lint.rs | 15 +- crates/forge/tests/ui.rs | 14 ++ crates/lint/Cargo.toml | 2 +- crates/lint/src/linter.rs | 2 +- crates/lint/src/sol/gas/keccack.rs | 28 +-- crates/lint/src/sol/gas/mod.rs | 4 +- crates/lint/src/sol/high/incorrect_shift.rs | 23 --- crates/lint/src/sol/high/mod.rs | 2 +- crates/lint/src/sol/info/mixed_case.rs | 38 +--- crates/lint/src/sol/info/mod.rs | 10 +- crates/lint/src/sol/info/pascal_case.rs | 27 +-- .../lint/src/sol/info/screaming_snake_case.rs | 65 +++---- crates/lint/src/sol/macros.rs | 16 +- crates/lint/src/sol/med/div_mul.rs | 24 --- crates/lint/src/sol/med/mod.rs | 2 +- crates/lint/src/sol/mod.rs | 49 ++++-- crates/lint/testdata/DivideBeforeMultiply.sol | 28 +-- .../lint/testdata/DivideBeforeMultiply.stderr | 42 +++++ crates/lint/testdata/IncorrectShift.sol | 12 +- crates/lint/testdata/IncorrectShift.stderr | 35 ++++ crates/lint/testdata/Keccak256.sol | 4 +- crates/lint/testdata/Keccak256.stderr | 14 ++ crates/lint/testdata/MixedCase.sol | 24 +-- crates/lint/testdata/MixedCase.stderr | 66 +++++++ crates/lint/testdata/ScreamingSnakeCase.sol | 22 +-- .../lint/testdata/ScreamingSnakeCase.stderr | 60 +++++++ crates/lint/testdata/StructPascalCase.sol | 14 +- crates/lint/testdata/StructPascalCase.stderr | 48 +++++ crates/test-utils/Cargo.toml | 4 + crates/test-utils/src/lib.rs | 3 + crates/test-utils/src/runner.rs | 158 +++++++++++++++++ 33 files changed, 736 insertions(+), 289 deletions(-) create mode 100644 crates/forge/tests/ui.rs create mode 100644 crates/lint/testdata/DivideBeforeMultiply.stderr create mode 100644 crates/lint/testdata/IncorrectShift.stderr create mode 100644 crates/lint/testdata/Keccak256.stderr create mode 100644 crates/lint/testdata/MixedCase.stderr create mode 100644 crates/lint/testdata/ScreamingSnakeCase.stderr create mode 100644 crates/lint/testdata/StructPascalCase.stderr create mode 100644 crates/test-utils/src/runner.rs diff --git a/Cargo.lock b/Cargo.lock index 2ee7b2dd28cf2..3c2e429ddd3a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -557,7 +557,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "alloy-sol-types", - "itertools 0.13.0", + "itertools 0.14.0", "serde", "serde_json", "thiserror 2.0.12", @@ -2124,6 +2124,38 @@ dependencies = [ "serde", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.26", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -2546,6 +2578,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "comfy-table" version = "7.1.4" @@ -2557,6 +2599,12 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "comma" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" + [[package]] name = "compact_str" version = "0.8.1" @@ -2710,6 +2758,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -3297,7 +3354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3416,7 +3473,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3978,7 +4035,7 @@ dependencies = [ "fs_extra", "futures-util", "home", - "itertools 0.13.0", + "itertools 0.14.0", "path-slash", "rand 0.8.5", "rayon", @@ -4330,6 +4387,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "ui_test", ] [[package]] @@ -5428,7 +5486,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5500,7 +5558,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5684,6 +5742,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "libc" version = "0.2.172" @@ -6426,6 +6490,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "parity-scale-codec" version = "3.7.4" @@ -6809,6 +6882,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettydiff" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abec3fb083c10660b3854367697da94c674e9e82aa7511014dc958beeb7215e9" +dependencies = [ + "owo-colors", + "pad", +] + [[package]] name = "prettyplease" version = "0.2.32" @@ -6975,7 +7058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.101", @@ -7118,7 +7201,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7650,6 +7733,18 @@ dependencies = [ "semver 1.0.26", ] +[[package]] +name = "rustfix" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + [[package]] name = "rustix" version = "0.38.44" @@ -7660,7 +7755,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7673,7 +7768,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8381,17 +8476,19 @@ dependencies = [ "derive_builder", "derive_more 2.0.1", "dunce", - "itertools 0.10.5", + "itertools 0.14.0", "itoa", "lasso", "match_cfg", "normalize-path", "rayon", "scoped-tls", + "serde", + "serde_json", "solar-config", "solar-data-structures", "solar-macros", - "thiserror 1.0.69", + "thiserror 2.0.12", "tracing", "unicode-width 0.2.0", ] @@ -8414,7 +8511,7 @@ dependencies = [ "alloy-primitives 1.1.0", "bitflags 2.9.0", "bumpalo", - "itertools 0.10.5", + "itertools 0.14.0", "memchr", "num-bigint", "num-rational", @@ -8503,6 +8600,16 @@ dependencies = [ "zip-extract", ] +[[package]] +name = "spanned" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86af297923fbcfd107c20a189a6e9c872160df71a7190ae4a7a6c5dce4b2feb6" +dependencies = [ + "bstr", + "color-eyre", +] + [[package]] name = "spin" version = "0.9.8" @@ -8733,7 +8840,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.12", "url", "zip", ] @@ -8831,7 +8938,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9507,6 +9614,32 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "ui_test" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1211b1111c752c73b33073d2958072be08825fd97c9ab4d83444da361a06634b" +dependencies = [ + "annotate-snippets", + "anyhow", + "bstr", + "cargo-platform", + "cargo_metadata", + "color-eyre", + "colored", + "comma", + "crossbeam-channel", + "indicatif", + "levenshtein", + "prettydiff", + "regex", + "rustc_version 0.4.1", + "rustfix", + "serde", + "serde_json", + "spanned", +] + [[package]] name = "uint" version = "0.9.5" @@ -10059,7 +10192,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index a33e65de74f7a..2c78163606146 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -33,6 +33,10 @@ pub struct LintArgs { /// `exclude_lints` project config. #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))] lint: Option>, + + /// Activates the linter's JSON formatter (rustc-compatible). + #[arg(long)] + json: bool, } impl_figment_convert_basic!(LintArgs); @@ -99,6 +103,7 @@ impl LintArgs { if project.compiler.solc.is_some() { let linter = SolidityLinter::new() + .with_json_emitter(self.json) .with_description(true) .with_lints(include) .without_lints(exclude) diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index 4250aaa3307d4..7f125e9877867 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -47,13 +47,12 @@ forgetest!(can_use_config, |prj, cmd| { }; }); cmd.arg("lint").assert_success().stderr_eq(str![[r#" -warning[divide-before-multiply] +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision [FILE]:16:9 | 16 | (1 / 2) * 3; | ----------- | - = help: multiplication should occur before division to avoid loss of precision "#]]); @@ -70,13 +69,12 @@ forgetest!(can_use_config_ignore, |prj, cmd| { LinterConfig { ignore: vec!["src/ContractWithLints.sol".into()], ..Default::default() }; }); cmd.arg("lint").assert_success().stderr_eq(str![[r#" -note[mixed-case-variable] +note[mixed-case-variable]: mutable variables should use mixedCase [FILE]:6:9 | 6 | uint256 VARIABLE_MIXED_CASE_INFO; | --------------------------------- | - = help: mutable variables should use mixedCase "#]]); @@ -105,13 +103,12 @@ forgetest!(can_override_config_severity, |prj, cmd| { }; }); cmd.arg("lint").args(["--severity", "info"]).assert_success().stderr_eq(str![[r#" -note[mixed-case-variable] +note[mixed-case-variable]: mutable variables should use mixedCase [FILE]:6:9 | 6 | uint256 VARIABLE_MIXED_CASE_INFO; | --------------------------------- | - = help: mutable variables should use mixedCase "#]]); @@ -131,13 +128,12 @@ forgetest!(can_override_config_path, |prj, cmd| { }; }); cmd.arg("lint").arg("src/ContractWithLints.sol").assert_success().stderr_eq(str![[r#" -warning[divide-before-multiply] +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision [FILE]:16:9 | 16 | (1 / 2) * 3; | ----------- | - = help: multiplication should occur before division to avoid loss of precision "#]]); @@ -158,13 +154,12 @@ forgetest!(can_override_config_lint, |prj, cmd| { }); cmd.arg("lint").args(["--only-lint", "incorrect-shift"]).assert_success().stderr_eq(str![[ r#" -warning[incorrect-shift] +warning[incorrect-shift]: the order of args in a shift operation is incorrect [FILE]:13:18 | 13 | result = 8 >> localValue; | --------------- | - = help: the order of args in a shift operation is incorrect "# diff --git a/crates/forge/tests/ui.rs b/crates/forge/tests/ui.rs new file mode 100644 index 0000000000000..98053a45ac95b --- /dev/null +++ b/crates/forge/tests/ui.rs @@ -0,0 +1,14 @@ +use foundry_test_utils::runner; +use std::path::Path; + +const FORGE_CMD: &'static str = env!("CARGO_BIN_EXE_forge"); +const FORGE_DIR: &'static str = env!("CARGO_MANIFEST_DIR"); + +#[test] +fn forge_lint_ui_tests() -> eyre::Result<()> { + let forge_cmd = Path::new(FORGE_CMD); + let forge_dir = Path::new(FORGE_DIR); + let lint_testdata = forge_dir.parent().unwrap().join("lint").join("testdata"); + + runner::run_tests("lint", &forge_cmd, &lint_testdata, true) +} diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 7f9e4a84f59b9..b5568847aed71 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -21,7 +21,7 @@ foundry-config.workspace = true solar-parse.workspace = true solar-ast.workspace = true -solar-interface.workspace = true +solar-interface = { workspace = true, features = ["json"] } heck.workspace = true eyre.workspace = true diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index 0efb7a9eebf8a..6c188ff86497f 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -50,7 +50,7 @@ impl<'s> LintContext<'s> { pub fn emit(&self, lint: &'static L, span: Span) { let (desc, help) = match (self.desc, lint.help()) { (true, Some(help)) => (lint.description(), help), - (true, None) => ("", lint.description()), + (true, None) => (lint.description(), ""), (false, _) => ("", ""), }; diff --git a/crates/lint/src/sol/gas/keccack.rs b/crates/lint/src/sol/gas/keccack.rs index 144f8fac6f2cd..94a7859b6dfc1 100644 --- a/crates/lint/src/sol/gas/keccack.rs +++ b/crates/lint/src/sol/gas/keccack.rs @@ -9,9 +9,9 @@ use crate::{ }; declare_forge_lint!( - ASM_KECCACK256, + ASM_KECCAK256, Severity::Gas, - "asm-keccack256", + "asm-keccak256", "hash using inline assembly to save gas", "" ); @@ -21,31 +21,9 @@ impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { if ident.name == Keccak256 { - ctx.emit(&ASM_KECCACK256, expr.span); + ctx.emit(&ASM_KECCAK256, expr.span); } } } } } - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{linter::Lint, sol::SolidityLinter}; - - #[test] - fn test_keccak256() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![ASM_KECCACK256])); - - let emitted = linter.lint_test(Path::new("testdata/Keccak256.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning[{}]", ASM_KECCACK256.id())).count(); - let notes = emitted.matches(&format!("note[{}]", ASM_KECCACK256.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 2, "Expected 2 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/gas/mod.rs b/crates/lint/src/sol/gas/mod.rs index 916ae2b34dca3..4f7545feb2cab 100644 --- a/crates/lint/src/sol/gas/mod.rs +++ b/crates/lint/src/sol/gas/mod.rs @@ -1,9 +1,9 @@ mod keccack; -use keccack::ASM_KECCACK256; +use keccack::ASM_KECCAK256; use crate::{ register_lints, sol::{EarlyLintPass, SolLint}, }; -register_lints!((AsmKeccak256, ASM_KECCACK256)); +register_lints!((AsmKeccak256, (ASM_KECCAK256))); diff --git a/crates/lint/src/sol/high/incorrect_shift.rs b/crates/lint/src/sol/high/incorrect_shift.rs index e43cc8f3d019b..632a8aa7f1d7f 100644 --- a/crates/lint/src/sol/high/incorrect_shift.rs +++ b/crates/lint/src/sol/high/incorrect_shift.rs @@ -42,26 +42,3 @@ fn contains_incorrect_shift<'ast>( is_left_literal && is_right_not_literal } - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{linter::Lint, sol::SolidityLinter}; - - #[test] - fn test_incorrect_shift() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![INCORRECT_SHIFT])); - - let emitted = - linter.lint_test(Path::new("testdata/IncorrectShift.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning[{}]", INCORRECT_SHIFT.id())).count(); - let notes = emitted.matches(&format!("note[{}]", INCORRECT_SHIFT.id())).count(); - - assert_eq!(warnings, 5, "Expected 5 warnings"); - assert_eq!(notes, 0, "Expected 0 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/high/mod.rs b/crates/lint/src/sol/high/mod.rs index e6517bdcf9adb..475ada5f24685 100644 --- a/crates/lint/src/sol/high/mod.rs +++ b/crates/lint/src/sol/high/mod.rs @@ -6,4 +6,4 @@ use crate::{ sol::{EarlyLintPass, SolLint}, }; -register_lints!((IncorrectShift, INCORRECT_SHIFT)); +register_lints!((IncorrectShift, (INCORRECT_SHIFT))); diff --git a/crates/lint/src/sol/info/mixed_case.rs b/crates/lint/src/sol/info/mixed_case.rs index d91b57ab5ea3b..27a775ca5e9be 100644 --- a/crates/lint/src/sol/info/mixed_case.rs +++ b/crates/lint/src/sol/info/mixed_case.rs @@ -11,7 +11,7 @@ declare_forge_lint!( MIXED_CASE_FUNCTION, Severity::Info, "mixed-case-function", - "function names should use mixedCase.", + "function names should use mixedCase", "https://docs.soliditylang.org/en/latest/style-guide.html#function-names" ); @@ -62,39 +62,3 @@ pub fn is_mixed_case(s: &str) -> bool { // Remove leading/trailing underscores like `heck` does s.trim_matches('_') == format!("{}", heck::AsLowerCamelCase(s)).as_str() } - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{linter::Lint, sol::SolidityLinter}; - - #[test] - fn test_variable_mixed_case() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![MIXED_CASE_VARIABLE])); - - let emitted = linter.lint_test(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning[{}]", MIXED_CASE_VARIABLE.id())).count(); - let notes = emitted.matches(&format!("note[{}]", MIXED_CASE_VARIABLE.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 6, "Expected 6 notes"); - - Ok(()) - } - - #[test] - fn test_function_mixed_case() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![MIXED_CASE_FUNCTION])); - - let emitted = linter.lint_test(Path::new("testdata/MixedCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning[{}]", MIXED_CASE_FUNCTION.id())).count(); - let notes = emitted.matches(&format!("note[{}]", MIXED_CASE_FUNCTION.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 3, "Expected 3 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index 7a57e1afa3351..9988d60586833 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -5,7 +5,7 @@ mod pascal_case; use pascal_case::PASCAL_CASE_STRUCT; mod screaming_snake_case; -use screaming_snake_case::SCREAMING_SNAKE_CASE; +use screaming_snake_case::{SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE}; use crate::{ register_lints, @@ -13,8 +13,8 @@ use crate::{ }; register_lints!( - (MixedCaseVariable, MIXED_CASE_VARIABLE), - (ScreamingSnakeCase, SCREAMING_SNAKE_CASE), - (PascalCaseStruct, PASCAL_CASE_STRUCT), - (MixedCaseFunction, MIXED_CASE_FUNCTION) + (PascalCaseStruct, (PASCAL_CASE_STRUCT)), + (MixedCaseVariable, (MIXED_CASE_VARIABLE)), + (MixedCaseFunction, (MIXED_CASE_FUNCTION)), + (ScreamingSnakeCase, (SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE)) ); diff --git a/crates/lint/src/sol/info/pascal_case.rs b/crates/lint/src/sol/info/pascal_case.rs index 4245e2e322636..861a4834dfcc9 100644 --- a/crates/lint/src/sol/info/pascal_case.rs +++ b/crates/lint/src/sol/info/pascal_case.rs @@ -10,8 +10,8 @@ use crate::{ declare_forge_lint!( PASCAL_CASE_STRUCT, Severity::Info, - "struct-pascal-case", - "structs should use PascalCase.", + "pascal-case-struct", + "structs should use PascalCase", "https://docs.soliditylang.org/en/latest/style-guide.html#struct-names" ); @@ -32,26 +32,3 @@ pub fn is_pascal_case(s: &str) -> bool { s == format!("{}", heck::AsPascalCase(s)).as_str() } - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{linter::Lint, sol::SolidityLinter}; - - #[test] - fn test_struct_pascal_case() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![PASCAL_CASE_STRUCT])); - - let emitted = - linter.lint_test(Path::new("testdata/StructPascalCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning[{}]", PASCAL_CASE_STRUCT.id())).count(); - let notes = emitted.matches(&format!("note[{}]", PASCAL_CASE_STRUCT.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 6, "Expected 7 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/info/screaming_snake_case.rs b/crates/lint/src/sol/info/screaming_snake_case.rs index cb2370563753c..ab253577a8e23 100644 --- a/crates/lint/src/sol/info/screaming_snake_case.rs +++ b/crates/lint/src/sol/info/screaming_snake_case.rs @@ -1,4 +1,4 @@ -use solar_ast::VariableDefinition; +use solar_ast::{Span, VarMut, VariableDefinition}; use super::ScreamingSnakeCase; use crate::{ @@ -8,11 +8,18 @@ use crate::{ }; declare_forge_lint!( - SCREAMING_SNAKE_CASE, + SCREAMING_SNAKE_CASE_CONSTANT, Severity::Info, - "screaming-snake-case", - "constants and immutables should use SCREAMING_SNAKE_CASE", - "https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names" + "screaming-snake-case-const", + "constants should use SCREAMING_SNAKE_CASE", + "https://docs.soliditylang.org/en/latest/style-guide.html#constants" +); + +declare_forge_lint!( + SCREAMING_SNAKE_CASE_IMMUTABLE, + Severity::Info, + "screaming-snake-case-immutable", + "immutables should use SCREAMING_SNAKE_CASE" ); impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { @@ -21,19 +28,28 @@ impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { ctx: &LintContext<'_>, var: &'ast VariableDefinition<'ast>, ) { - if let Some(mutability) = var.mutability { - if mutability.is_constant() || mutability.is_immutable() { - if let Some(name) = var.name { - let name = name.as_str(); - if !is_screaming_snake_case(name) && name.len() > 1 { - ctx.emit(&SCREAMING_SNAKE_CASE, var.span); - } - } + if let (Some(name), Some(mutability)) = (var.name, var.mutability) { + let name = name.as_str(); + if name.len() < 2 || is_screaming_snake_case(name) { + return (); + } + + match mutability { + VarMut::Constant => ctx.emit(&SCREAMING_SNAKE_CASE_CONSTANT, get_var_span(var)), + VarMut::Immutable => ctx.emit(&SCREAMING_SNAKE_CASE_IMMUTABLE, get_var_span(var)), } } } } +/// Get the variable name span if available. Otherwise default to the line span. +fn get_var_span<'ast>(var: &'ast VariableDefinition<'ast>) -> Span { + match var.name { + Some(ident) => ident.span, + None => var.span, + } +} + /// Check if a string is SCREAMING_SNAKE_CASE. Numbers don't need to be preceded by an underscore. pub fn is_screaming_snake_case(s: &str) -> bool { if s.len() <= 1 { @@ -43,26 +59,3 @@ pub fn is_screaming_snake_case(s: &str) -> bool { // Remove leading/trailing underscores like `heck` does s.trim_matches('_') == format!("{}", heck::AsShoutySnakeCase(s)).as_str() } - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{linter::Lint, sol::SolidityLinter}; - - #[test] - fn test_screaming_snake_case() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![SCREAMING_SNAKE_CASE])); - - let emitted = - linter.lint_test(Path::new("testdata/ScreamingSnakeCase.sol")).unwrap().to_string(); - let warnings = emitted.matches(&format!("warning[{}]", SCREAMING_SNAKE_CASE.id())).count(); - let notes = emitted.matches(&format!("note[{}]", SCREAMING_SNAKE_CASE.id())).count(); - - assert_eq!(warnings, 0, "Expected 0 warnings"); - assert_eq!(notes, 8, "Expected 8 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index 2da25bd69073d..093b96a7e4a70 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -30,7 +30,7 @@ macro_rules! declare_forge_lint { /// # Parameters /// /// - `$pass_id`: Identitifier of the generated struct that will implement the pass trait. -/// - `$lint`: `SolLint` constant. +/// - (`$lint`): tuple with `SolLint` constants that should be evaluated on every input that pass. /// /// # Outputs /// @@ -39,7 +39,7 @@ macro_rules! declare_forge_lint { /// - `const LINT_PASSES` mapping each lint to its corresponding pass #[macro_export] macro_rules! register_lints { - ($(($pass_id:ident, $lint:expr)),* $(,)?) => { + ( $( ($pass_id:ident, ($($lint:expr),+ $(,)?)) ),* $(,)? ) => { // Declare the structs that will implement the pass trait $( #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] @@ -53,21 +53,15 @@ macro_rules! register_lints { )* // Expose array constants - pub const REGISTERED_LINTS: &[SolLint] = &[$($lint),*]; + pub const REGISTERED_LINTS: &[SolLint] = &[$( $($lint,) + )*]; pub const LINT_PASSES: &[(SolLint, fn() -> Box>)] = &[ - $( - ($lint, || Box::new($pass_id::default())), - )* + $( $( ($lint, || Box::new($pass_id::default())), )+ )* ]; // Helper function to create lint passes with the required lifetime pub fn create_lint_passes<'a>() -> Vec<(Box>, SolLint)> { - vec![ - $( - ($pass_id::as_lint_pass(), $lint), - )* - ] + vec![ $( $(($pass_id::as_lint_pass(), $lint), )+ )* ] } }; } diff --git a/crates/lint/src/sol/med/div_mul.rs b/crates/lint/src/sol/med/div_mul.rs index 6c6b16927c7a0..eb8bf185ac079 100644 --- a/crates/lint/src/sol/med/div_mul.rs +++ b/crates/lint/src/sol/med/div_mul.rs @@ -38,27 +38,3 @@ fn contains_division<'ast>(expr: &'ast Expr<'ast>) -> bool { _ => false, } } - -#[cfg(test)] -mod test { - use std::path::Path; - - use super::*; - use crate::{linter::Lint, sol::SolidityLinter}; - - #[test] - fn test_divide_before_multiply() -> eyre::Result<()> { - let linter = SolidityLinter::new().with_lints(Some(vec![DIVIDE_BEFORE_MULTIPLY])); - - let emitted = - linter.lint_test(Path::new("testdata/DivideBeforeMultiply.sol")).unwrap().to_string(); - let warnings = - emitted.matches(&format!("warning[{}]", DIVIDE_BEFORE_MULTIPLY.id())).count(); - let notes = emitted.matches(&format!("note[{}]", DIVIDE_BEFORE_MULTIPLY.id())).count(); - - assert_eq!(warnings, 6, "Expected 6 warnings"); - assert_eq!(notes, 0, "Expected 0 notes"); - - Ok(()) - } -} diff --git a/crates/lint/src/sol/med/mod.rs b/crates/lint/src/sol/med/mod.rs index 7b0888575bd6d..d81ac6c4df242 100644 --- a/crates/lint/src/sol/med/mod.rs +++ b/crates/lint/src/sol/med/mod.rs @@ -6,4 +6,4 @@ use crate::{ sol::{EarlyLintPass, SolLint}, }; -register_lints!((DivideBeforeMultiply, DIVIDE_BEFORE_MULTIPLY)); +register_lints!((DivideBeforeMultiply, (DIVIDE_BEFORE_MULTIPLY))); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index abd052fe86445..6ada6dd33f5ef 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -11,8 +11,14 @@ use foundry_compilers::solc::SolcLanguage; use foundry_config::lint::Severity; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena}; -use solar_interface::{diagnostics, Session}; -use std::path::{Path, PathBuf}; +use solar_interface::{ + diagnostics::{self, DiagCtxt, JsonEmitter}, + Session, SourceMap, +}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use thiserror::Error; /// Linter implementation to analyze Solidity source code responsible for identifying @@ -23,11 +29,18 @@ pub struct SolidityLinter { lints_included: Option>, lints_excluded: Option>, with_description: bool, + with_json_emitter: bool, } impl SolidityLinter { pub fn new() -> Self { - Self { severity: None, lints_included: None, lints_excluded: None, with_description: true } + Self { + severity: None, + lints_included: None, + lints_excluded: None, + with_description: true, + with_json_emitter: false, + } } pub fn with_severity(mut self, severity: Option>) -> Self { @@ -50,17 +63,9 @@ impl SolidityLinter { self } - #[cfg(test)] - /// Helper function to ease testing, despite `fn lint` being the public API for the `Linter`. - /// Logs the diagnostics to the local buffer, so that tests can perform assertions. - pub(crate) fn lint_test(&self, file: &Path) -> Option { - let mut sess = - Session::builder().with_buffer_emitter(solar_interface::ColorChoice::Never).build(); - sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); - - self.process_file(&sess, file); - - sess.emitted_diagnostics() + pub fn with_json_emitter(mut self, with: bool) -> Self { + self.with_json_emitter = with; + self } fn process_file(&self, sess: &Session, file: &Path) { @@ -118,8 +123,22 @@ impl Linter for SolidityLinter { type Lint = SolLint; fn lint(&self, input: &[PathBuf]) { + let mut builder = Session::builder(); + + // Build session based on the linter config + if self.with_json_emitter { + let map = Arc::::default(); + let json_emitter = JsonEmitter::new(Box::new(std::io::stderr()), map.clone()) + .rustc_like(true) + .ui_testing(false); + + builder = builder.dcx(DiagCtxt::new(Box::new(json_emitter))).source_map(map); + } else { + builder = builder.with_stderr_emitter(); + }; + // Create a single session for all files - let mut sess = Session::builder().with_stderr_emitter().build(); + let mut sess = builder.build(); sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false); // Process the files in parallel diff --git a/crates/lint/testdata/DivideBeforeMultiply.sol b/crates/lint/testdata/DivideBeforeMultiply.sol index 3bde4b9cf6c9f..694558bfca363 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.sol +++ b/crates/lint/testdata/DivideBeforeMultiply.sol @@ -1,18 +1,18 @@ contract DivideBeforeMultiply { function arithmetic() public { - (1 / 2) * 3; // Unsafe - (1 * 2) / 3; // Safe - ((1 / 2) * 3) * 4; // Unsafe - ((1 * 2) / 3) * 4; // Unsafe - (1 / 2 / 3) * 4; // Unsafe - (1 / (2 + 3)) * 4; // Unsafe - (1 / 2 + 3) * 4; // Safe - (1 / 2 - 3) * 4; // Safe - (1 + 2 / 3) * 4; // Safe - (1 / 2 - 3) * 4; // Safe - ((1 / 2) % 3) * 4; // Safe - 1 / (2 * 3 + 3); // Safe - 1 / ((2 / 3) * 3); // Unsafe - 1 / ((2 * 3) + 3); // Safe + (1 / 2) * 3; //~WARN: multiplication should occur before division to avoid loss of precision + (1 * 2) / 3; + ((1 / 2) * 3) * 4; //~WARN: multiplication should occur before division to avoid loss of precision + ((1 * 2) / 3) * 4; //~WARN: multiplication should occur before division to avoid loss of precision + (1 / 2 / 3) * 4; //~WARN: multiplication should occur before division to avoid loss of precision + (1 / (2 + 3)) * 4; //~WARN: multiplication should occur before division to avoid loss of precision + (1 / 2 + 3) * 4; + (1 / 2 - 3) * 4; + (1 + 2 / 3) * 4; + (1 / 2 - 3) * 4; + ((1 / 2) % 3) * 4; + 1 / (2 * 3 + 3); + 1 / ((2 / 3) * 3); //~WARN: multiplication should occur before division to avoid loss of precision + 1 / ((2 * 3) + 3); } } diff --git a/crates/lint/testdata/DivideBeforeMultiply.stderr b/crates/lint/testdata/DivideBeforeMultiply.stderr new file mode 100644 index 0000000000000..08d8ebe25d1c9 --- /dev/null +++ b/crates/lint/testdata/DivideBeforeMultiply.stderr @@ -0,0 +1,42 @@ +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision + --> ROOT/testdata/DivideBeforeMultiply.sol:LL:CC + | +3 | (1 / 2) * 3; + | ----------- + | + +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision + --> ROOT/testdata/DivideBeforeMultiply.sol:LL:CC + | +5 | ((1 / 2) * 3) * 4; + | ----------- + | + +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision + --> ROOT/testdata/DivideBeforeMultiply.sol:LL:CC + | +6 | ((1 * 2) / 3) * 4; + | ----------------- + | + +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision + --> ROOT/testdata/DivideBeforeMultiply.sol:LL:CC + | +7 | (1 / 2 / 3) * 4; + | --------------- + | + +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision + --> ROOT/testdata/DivideBeforeMultiply.sol:LL:CC + | +8 | (1 / (2 + 3)) * 4; + | ----------------- + | + +warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision + --> ROOT/testdata/DivideBeforeMultiply.sol:LL:CC + | +15 | 1 / ((2 / 3) * 3); + | ----------- + | + diff --git a/crates/lint/testdata/IncorrectShift.sol b/crates/lint/testdata/IncorrectShift.sol index 6dc1edb2b46a0..9377354851c42 100644 --- a/crates/lint/testdata/IncorrectShift.sol +++ b/crates/lint/testdata/IncorrectShift.sol @@ -18,14 +18,14 @@ contract IncorrectShift { // - Literal << NonLiteral // - Literal >> NonLiteral - result = 2 << stateValue; - result = 8 >> localValue; - result = 16 << (stateValue + 1); - result = 32 >> getAmount(); - result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); + result = 2 << stateValue; //~WARN: the order of args in a shift operation is incorrect + result = 8 >> localValue; //~WARN: the order of args in a shift operation is incorrect + result = 16 << (stateValue + 1); //~WARN: the order of args in a shift operation is incorrect + result = 32 >> getAmount(); //~WARN: the order of args in a shift operation is incorrect + result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); //~WARN: the order of args in a shift operation is incorrect // SHOULD PASS: - result = stateValue << 2; + result = stateValue << 2; result = localValue >> 3; result = stateValue << localShiftAmount; result = localValue >> stateShiftAmount; diff --git a/crates/lint/testdata/IncorrectShift.stderr b/crates/lint/testdata/IncorrectShift.stderr new file mode 100644 index 0000000000000..16fbda627cd9b --- /dev/null +++ b/crates/lint/testdata/IncorrectShift.stderr @@ -0,0 +1,35 @@ +warning[incorrect-shift]: the order of args in a shift operation is incorrect + --> ROOT/testdata/IncorrectShift.sol:LL:CC + | +21 | result = 2 << stateValue; + | --------------- + | + +warning[incorrect-shift]: the order of args in a shift operation is incorrect + --> ROOT/testdata/IncorrectShift.sol:LL:CC + | +22 | result = 8 >> localValue; + | --------------- + | + +warning[incorrect-shift]: the order of args in a shift operation is incorrect + --> ROOT/testdata/IncorrectShift.sol:LL:CC + | +23 | result = 16 << (stateValue + 1); + | ---------------------- + | + +warning[incorrect-shift]: the order of args in a shift operation is incorrect + --> ROOT/testdata/IncorrectShift.sol:LL:CC + | +24 | result = 32 >> getAmount(); + | ----------------- + | + +warning[incorrect-shift]: the order of args in a shift operation is incorrect + --> ROOT/testdata/IncorrectShift.sol:LL:CC + | +25 | ... result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); + | ------------------------------------------------------------ + | + diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol index 12fcbc4c00c21..8d3a3044bc8cd 100644 --- a/crates/lint/testdata/Keccak256.sol +++ b/crates/lint/testdata/Keccak256.sol @@ -1,10 +1,10 @@ contract AsmKeccak256 { constructor(uint256 a, uint256 b) { - keccak256(abi.encodePacked(a, b)); + keccak256(abi.encodePacked(a, b)); //~NOTE: hash using inline assembly to save gas } function solidityHash(uint256 a, uint256 b) public view { - keccak256(abi.encodePacked(a, b)); + keccak256(abi.encodePacked(a, b)); //~NOTE: hash using inline assembly to save gas } function assemblyHash(uint256 a, uint256 b) public view { diff --git a/crates/lint/testdata/Keccak256.stderr b/crates/lint/testdata/Keccak256.stderr new file mode 100644 index 0000000000000..e589bec00eb77 --- /dev/null +++ b/crates/lint/testdata/Keccak256.stderr @@ -0,0 +1,14 @@ +note[asm-keccak256]: hash using inline assembly to save gas + --> ROOT/testdata/Keccak256.sol:LL:CC + | +3 | keccak256(abi.encodePacked(a, b)); + | --------- + | + +note[asm-keccak256]: hash using inline assembly to save gas + --> ROOT/testdata/Keccak256.sol:LL:CC + | +7 | keccak256(abi.encodePacked(a, b)); + | --------- + | + diff --git a/crates/lint/testdata/MixedCase.sol b/crates/lint/testdata/MixedCase.sol index 7cd68f0a5862e..871922867affe 100644 --- a/crates/lint/testdata/MixedCase.sol +++ b/crates/lint/testdata/MixedCase.sol @@ -2,34 +2,28 @@ pragma solidity ^0.8.0; contract MixedCaseTest { - // Passes uint256 variableMixedCase; uint256 _variableMixedCase; uint256 variablemixedcase; - // Fails - uint256 Variablemixedcase; - uint256 VARIABLE_MIXED_CASE; - uint256 VariableMixedCase; + uint256 Variablemixedcase; //~NOTE: mutable variables should use mixedCase + uint256 VARIABLE_MIXED_CASE; //~NOTE: mutable variables should use mixedCase + uint256 VariableMixedCase; //~NOTE: mutable variables should use mixedCase function foo() public { - // Passes uint256 testVal; uint256 testVal123; - // Fails - uint256 testVAL; - uint256 TestVal; - uint256 TESTVAL; + uint256 testVAL; //~NOTE: mutable variables should use mixedCase + uint256 TestVal; //~NOTE: mutable variables should use mixedCase + uint256 TESTVAL; //~NOTE: mutable variables should use mixedCase } - // Passes function functionMixedCase() public {} function _functionMixedCase() internal {} function functionmixedcase() public {} - // Fails - function Functionmixedcase() public {} - function FUNCTION_MIXED_CASE() public {} - function FunctionMixedCase() public {} + function Functionmixedcase() public {} //~NOTE: function names should use mixedCase + function FUNCTION_MIXED_CASE() public {} //~NOTE: function names should use mixedCase + function FunctionMixedCase() public {} //~NOTE: function names should use mixedCase } diff --git a/crates/lint/testdata/MixedCase.stderr b/crates/lint/testdata/MixedCase.stderr new file mode 100644 index 0000000000000..a5ef41dec2376 --- /dev/null +++ b/crates/lint/testdata/MixedCase.stderr @@ -0,0 +1,66 @@ +note[mixed-case-variable]: mutable variables should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +9 | uint256 Variablemixedcase; + | -------------------------- + | + +note[mixed-case-variable]: mutable variables should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +10 | uint256 VARIABLE_MIXED_CASE; + | ---------------------------- + | + +note[mixed-case-variable]: mutable variables should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +11 | uint256 VariableMixedCase; + | -------------------------- + | + +note[mixed-case-variable]: mutable variables should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +17 | uint256 testVAL; + | --------------- + | + +note[mixed-case-variable]: mutable variables should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +18 | uint256 TestVal; + | --------------- + | + +note[mixed-case-variable]: mutable variables should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +19 | uint256 TESTVAL; + | --------------- + | + +note[mixed-case-function]: function names should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +26 | function Functionmixedcase() public {} + | -- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#function-names + +note[mixed-case-function]: function names should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +27 | function FUNCTION_MIXED_CASE() public {} + | -- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#function-names + +note[mixed-case-function]: function names should use mixedCase + --> ROOT/testdata/MixedCase.sol:LL:CC + | +28 | function FunctionMixedCase() public {} + | -- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#function-names + diff --git a/crates/lint/testdata/ScreamingSnakeCase.sol b/crates/lint/testdata/ScreamingSnakeCase.sol index 2403fa4b52fb2..ccfe596d9e9dd 100644 --- a/crates/lint/testdata/ScreamingSnakeCase.sol +++ b/crates/lint/testdata/ScreamingSnakeCase.sol @@ -2,22 +2,22 @@ pragma solidity ^0.8.0; contract ScreamingSnakeCaseTest { - // Passes uint256 constant _SCREAMING_SNAKE_CASE = 0; uint256 constant SCREAMING_SNAKE_CASE = 0; + uint256 constant SCREAMINGSNAKECASE = 0; + + uint256 constant screamingSnakeCase = 0; //~NOTE: constants should use SCREAMING_SNAKE_CASE + uint256 constant screaming_snake_case = 0; //~NOTE: constants should use SCREAMING_SNAKE_CASE + uint256 constant ScreamingSnakeCase = 0; //~NOTE: constants should use SCREAMING_SNAKE_CASE + uint256 constant SCREAMING_snake_case = 0; //~NOTE: constants should use SCREAMING_SNAKE_CASE + uint256 immutable _SCREAMING_SNAKE_CASE_1 = 0; uint256 immutable SCREAMING_SNAKE_CASE_1 = 0; - uint256 constant SCREAMINGSNAKECASE = 0; uint256 immutable SCREAMINGSNAKECASE0 = 0; uint256 immutable SCREAMINGSNAKECASE_ = 0; - // Fails - uint256 constant screamingSnakeCase = 0; - uint256 constant screaming_snake_case = 0; - uint256 constant ScreamingSnakeCase = 0; - uint256 constant SCREAMING_snake_case = 0; - uint256 immutable screamingSnakeCase0 = 0; - uint256 immutable screaming_snake_case0 = 0; - uint256 immutable ScreamingSnakeCase0 = 0; - uint256 immutable SCREAMING_snake_case_0 = 0; + uint256 immutable screamingSnakeCase0 = 0; //~NOTE: immutables should use SCREAMING_SNAKE_CASE + uint256 immutable screaming_snake_case0 = 0; //~NOTE: immutables should use SCREAMING_SNAKE_CASE + uint256 immutable ScreamingSnakeCase0 = 0; //~NOTE: immutables should use SCREAMING_SNAKE_CASE + uint256 immutable SCREAMING_snake_case_0 = 0; //~NOTE: immutables should use SCREAMING_SNAKE_CASE } diff --git a/crates/lint/testdata/ScreamingSnakeCase.stderr b/crates/lint/testdata/ScreamingSnakeCase.stderr new file mode 100644 index 0000000000000..96c2cb34405b3 --- /dev/null +++ b/crates/lint/testdata/ScreamingSnakeCase.stderr @@ -0,0 +1,60 @@ +note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +9 | uint256 constant screamingSnakeCase = 0; + | ------------------ + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#constants + +note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +10 | uint256 constant screaming_snake_case = 0; + | -------------------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#constants + +note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +11 | uint256 constant ScreamingSnakeCase = 0; + | ------------------ + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#constants + +note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +12 | uint256 constant SCREAMING_snake_case = 0; + | -------------------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#constants + +note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +19 | uint256 immutable screamingSnakeCase0 = 0; + | ------------------- + | + +note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +20 | uint256 immutable screaming_snake_case0 = 0; + | --------------------- + | + +note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +21 | uint256 immutable ScreamingSnakeCase0 = 0; + | ------------------- + | + +note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE + --> ROOT/testdata/ScreamingSnakeCase.sol:LL:CC + | +22 | uint256 immutable SCREAMING_snake_case_0 = 0; + | ---------------------- + | + diff --git a/crates/lint/testdata/StructPascalCase.sol b/crates/lint/testdata/StructPascalCase.sol index d7322c1e7e17c..0ec638efe8211 100644 --- a/crates/lint/testdata/StructPascalCase.sol +++ b/crates/lint/testdata/StructPascalCase.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; contract StructPascalCaseTest { - // Passes struct PascalCase { uint256 a; } @@ -11,28 +10,27 @@ contract StructPascalCaseTest { uint256 a; } - // Fails - struct _PascalCase { + struct _PascalCase { //~NOTE: structs should use PascalCase uint256 a; } - struct pascalCase { + struct pascalCase { //~NOTE: structs should use PascalCase uint256 a; } - struct pascalcase { + struct pascalcase { //~NOTE: structs should use PascalCase uint256 a; } - struct pascal_case { + struct pascal_case { //~NOTE: structs should use PascalCase uint256 a; } - struct PASCAL_CASE { + struct PASCAL_CASE { //~NOTE: structs should use PascalCase uint256 a; } - struct PASCALCASE { + struct PASCALCASE { //~NOTE: structs should use PascalCase uint256 a; } } diff --git a/crates/lint/testdata/StructPascalCase.stderr b/crates/lint/testdata/StructPascalCase.stderr new file mode 100644 index 0000000000000..2ef869c7ccc21 --- /dev/null +++ b/crates/lint/testdata/StructPascalCase.stderr @@ -0,0 +1,48 @@ +note[pascal-case-struct]: structs should use PascalCase + --> ROOT/testdata/StructPascalCase.sol:LL:CC + | +13 | struct _PascalCase { + | ----------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#struct-names + +note[pascal-case-struct]: structs should use PascalCase + --> ROOT/testdata/StructPascalCase.sol:LL:CC + | +17 | struct pascalCase { + | ---------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#struct-names + +note[pascal-case-struct]: structs should use PascalCase + --> ROOT/testdata/StructPascalCase.sol:LL:CC + | +21 | struct pascalcase { + | ---------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#struct-names + +note[pascal-case-struct]: structs should use PascalCase + --> ROOT/testdata/StructPascalCase.sol:LL:CC + | +25 | struct pascal_case { + | ----------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#struct-names + +note[pascal-case-struct]: structs should use PascalCase + --> ROOT/testdata/StructPascalCase.sol:LL:CC + | +29 | struct PASCAL_CASE { + | ----------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#struct-names + +note[pascal-case-struct]: structs should use PascalCase + --> ROOT/testdata/StructPascalCase.sol:LL:CC + | +33 | struct PASCALCASE { + | ---------- + | + = help: https://docs.soliditylang.org/en/latest/style-guide.html#struct-names + diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index b9d7b1ce5f2f6..c6b45b29173e4 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -32,6 +32,7 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } rand.workspace = true snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] } tempfile.workspace = true +ui_test = "0.29.2" # See /Cargo.toml. idna_adapter.workspace = true @@ -39,3 +40,6 @@ idna_adapter.workspace = true [dev-dependencies] tokio.workspace = true foundry-block-explorers.workspace = true + +[patch.crates-io] +ui_test = { git = "https://github.com/oli-obk/ui_test", branch = "main" } diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index c1f717f2c118f..9f646d6765fec 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -30,6 +30,9 @@ pub use util::{TestCommand, TestProject}; mod script; pub use script::{ScriptOutcome, ScriptTester}; +// UI test runner +pub mod runner; + // re-exports for convenience pub use foundry_compilers; diff --git a/crates/test-utils/src/runner.rs b/crates/test-utils/src/runner.rs new file mode 100644 index 0000000000000..eef5fe66d80f9 --- /dev/null +++ b/crates/test-utils/src/runner.rs @@ -0,0 +1,158 @@ +use std::path::Path; +use ui_test::spanned::Spanned; + +/// Test runner based on `ui_test`. Adapted from `https://github.com/paradigmxyz/solar/tools/tester`. +pub fn run_tests<'a>( + cmd: &str, + cmd_path: &'a Path, + testdata: &'a Path, + bless: bool, +) -> eyre::Result<()> { + ui_test::color_eyre::install()?; + + let mut args = ui_test::Args::test()?; + args.bless = bless; + + // Fast path for `--list`, invoked by `cargo-nextest`. + { + let mut dummy_config = ui_test::Config::dummy(); + dummy_config.with_args(&args); + if ui_test::nextest::emulate(&mut vec![dummy_config]) { + return Ok(()); + } + } + + // Condense output if not explicitly requested. + let requested_pretty = || std::env::args().any(|x| x.contains("--format")); + if matches!(args.format, ui_test::Format::Pretty) && !requested_pretty() { + args.format = ui_test::Format::Terse; + } + + let config = config(cmd, cmd_path, &args, testdata); + + let text_emitter = match args.format { + ui_test::Format::Terse => ui_test::status_emitter::Text::quiet(), + ui_test::Format::Pretty => ui_test::status_emitter::Text::verbose(), + }; + let gha_emitter = ui_test::status_emitter::Gha:: { name: "Foundry Lint UI".to_string() }; + let status_emitter = (text_emitter, gha_emitter); + + // run tests on all .sol files + ui_test::run_tests_generic( + vec![config], + move |path, _config| Some(path.extension().map_or(false, |ext| ext == "sol")), + move |config, file_contents| per_file_config(config, file_contents), + status_emitter, + )?; + + Ok(()) +} + +fn config<'a>( + cmd: &str, + cmd_path: &'a Path, + args: &ui_test::Args, + testdata: &'a Path, +) -> ui_test::Config { + let root = testdata.parent().unwrap(); + assert!( + testdata.exists(), + "testdata directory does not exist: {};\n\ + you may need to initialize submodules: `git submodule update --init --checkout`", + testdata.display() + ); + + let mut config = ui_test::Config { + host: Some(get_host().to_string()), + target: None, + root_dir: testdata.into(), + program: ui_test::CommandBuilder { + program: cmd_path.into(), + args: { + let args = vec![cmd, "--json"]; + args.into_iter().map(Into::into).collect() + }, + out_dir_flag: None, + input_file_flag: None, + envs: vec![], + cfg_flag: None, + }, + output_conflict_handling: ui_test::error_on_output_conflict, + bless_command: Some("cargo uibless".into()), + out_dir: root.join("target/ui"), + comment_start: "//", + diagnostic_extractor: ui_test::diagnostics::rustc::rustc_diagnostics_extractor, + ..ui_test::Config::dummy() + }; + + macro_rules! register_custom_flags { + ($($ty:ty),* $(,)?) => { + $( + config.custom_comments.insert(<$ty>::NAME, <$ty>::parse); + if let Some(default) = <$ty>::DEFAULT { + config.comment_defaults.base().add_custom(<$ty>::NAME, default); + } + )* + }; + } + register_custom_flags![]; + + config.comment_defaults.base().exit_status = None.into(); + config.comment_defaults.base().require_annotations = Spanned::dummy(true).into(); + config.comment_defaults.base().require_annotations_for_level = + Spanned::dummy(ui_test::diagnostics::Level::Warn).into(); + + let filters = [ + (ui_test::Match::PathBackslash, b"/".to_vec()), + #[cfg(windows)] + (ui_test::Match::Exact(vec![b'\r']), b"".to_vec()), + #[cfg(windows)] + (ui_test::Match::Exact(br"\\?\".to_vec()), b"".to_vec()), + (root.into(), b"ROOT".to_vec()), + ]; + config.comment_defaults.base().normalize_stderr.extend(filters.iter().cloned()); + config.comment_defaults.base().normalize_stdout.extend(filters); + + let filters: &[(&str, &str)] = &[ + // Erase line and column info. + (r"\.(\w+):[0-9]+:[0-9]+(: [0-9]+:[0-9]+)?", ".$1:LL:CC"), + ]; + for &(pattern, replacement) in filters { + config.filter(pattern, replacement); + } + + let stdout_filters: &[(&str, &str)] = + &[(&env!("CARGO_PKG_VERSION").replace(".", r"\."), "VERSION")]; + for &(pattern, replacement) in stdout_filters { + config.stdout_filter(pattern, replacement); + } + let stderr_filters: &[(&str, &str)] = &[]; + for &(pattern, replacement) in stderr_filters { + config.stderr_filter(pattern, replacement); + } + + config.with_args(args); + config +} + +fn per_file_config(config: &mut ui_test::Config, file: &Spanned>) { + let Ok(src) = std::str::from_utf8(&file.content) else { + return; + }; + + assert_eq!(config.comment_start, "//"); + let has_annotations = src.contains("//~"); + config.comment_defaults.base().require_annotations = Spanned::dummy(has_annotations).into(); + let code = if has_annotations && src.contains("ERROR:") { 1 } else { 0 }; + config.comment_defaults.base().exit_status = Spanned::dummy(code).into(); +} + +fn get_host() -> &'static str { + static CACHE: std::sync::OnceLock = std::sync::OnceLock::new(); + CACHE.get_or_init(|| { + let mut config = ui_test::Config::dummy(); + config.program = ui_test::CommandBuilder::rustc(); + config.fill_host_and_target().unwrap(); + config.host.unwrap() + }) +} From 87c6829330fa5ea188eae18cb883dea24215a3b8 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 8 May 2025 10:19:13 +0200 Subject: [PATCH 091/107] style: clippy --- crates/forge/tests/ui.rs | 6 +++--- crates/lint/src/sol/info/screaming_snake_case.rs | 2 +- crates/test-utils/src/runner.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/forge/tests/ui.rs b/crates/forge/tests/ui.rs index 98053a45ac95b..bb95a4430c1fd 100644 --- a/crates/forge/tests/ui.rs +++ b/crates/forge/tests/ui.rs @@ -1,8 +1,8 @@ use foundry_test_utils::runner; use std::path::Path; -const FORGE_CMD: &'static str = env!("CARGO_BIN_EXE_forge"); -const FORGE_DIR: &'static str = env!("CARGO_MANIFEST_DIR"); +const FORGE_CMD: &str = env!("CARGO_BIN_EXE_forge"); +const FORGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); #[test] fn forge_lint_ui_tests() -> eyre::Result<()> { @@ -10,5 +10,5 @@ fn forge_lint_ui_tests() -> eyre::Result<()> { let forge_dir = Path::new(FORGE_DIR); let lint_testdata = forge_dir.parent().unwrap().join("lint").join("testdata"); - runner::run_tests("lint", &forge_cmd, &lint_testdata, true) + runner::run_tests("lint", forge_cmd, &lint_testdata, true) } diff --git a/crates/lint/src/sol/info/screaming_snake_case.rs b/crates/lint/src/sol/info/screaming_snake_case.rs index ab253577a8e23..d98af132a7ac1 100644 --- a/crates/lint/src/sol/info/screaming_snake_case.rs +++ b/crates/lint/src/sol/info/screaming_snake_case.rs @@ -31,7 +31,7 @@ impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { if let (Some(name), Some(mutability)) = (var.name, var.mutability) { let name = name.as_str(); if name.len() < 2 || is_screaming_snake_case(name) { - return (); + return; } match mutability { diff --git a/crates/test-utils/src/runner.rs b/crates/test-utils/src/runner.rs index eef5fe66d80f9..2f575d1d05502 100644 --- a/crates/test-utils/src/runner.rs +++ b/crates/test-utils/src/runner.rs @@ -40,8 +40,8 @@ pub fn run_tests<'a>( // run tests on all .sol files ui_test::run_tests_generic( vec![config], - move |path, _config| Some(path.extension().map_or(false, |ext| ext == "sol")), - move |config, file_contents| per_file_config(config, file_contents), + move |path, _config| Some(path.extension().is_some_and(|ext| ext == "sol")), + per_file_config, status_emitter, )?; From 04b111d42dcead91a2e095bd1649deca182ee8f1 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Thu, 8 May 2025 13:14:17 +0200 Subject: [PATCH 092/107] typo Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- crates/lint/src/sol/info/mixed_case.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/src/sol/info/mixed_case.rs b/crates/lint/src/sol/info/mixed_case.rs index 27a775ca5e9be..c4245282aedfd 100644 --- a/crates/lint/src/sol/info/mixed_case.rs +++ b/crates/lint/src/sol/info/mixed_case.rs @@ -52,7 +52,7 @@ impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable { /// Check if a string is mixedCase /// -/// To avoid false positives like `fn increment()` or `uin256 counter`, +/// To avoid false positives like `fn increment()` or `uint256 counter`, /// lowercase strings are treated as mixedCase. pub fn is_mixed_case(s: &str) -> bool { if s.len() <= 1 { From ad5a9cfb5640bbdbdf1102ff928a798d51b60f67 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 8 May 2025 16:07:33 +0200 Subject: [PATCH 093/107] fix: housekeeping --- Cargo.lock | 7 - crates/forge/src/cmd/lint.rs | 26 ++-- crates/forge/tests/ui.rs | 7 +- crates/lint/Cargo.toml | 7 - crates/lint/README.md | 120 ++++++++++++++++++ crates/lint/src/lib.rs | 6 +- .../src/sol/gas/{keccack.rs => keccak.rs} | 4 +- crates/lint/src/sol/gas/mod.rs | 4 +- .../lint/src/sol/info/screaming_snake_case.rs | 18 +-- crates/test-utils/src/lib.rs | 3 +- .../src/{runner.rs => ui_runner.rs} | 2 +- 11 files changed, 151 insertions(+), 53 deletions(-) create mode 100644 crates/lint/README.md rename crates/lint/src/sol/gas/{keccack.rs => keccak.rs} (88%) rename crates/test-utils/src/{runner.rs => ui_runner.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 3c2e429ddd3a7..d4d38b3a12453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3690,21 +3690,14 @@ dependencies = [ name = "forge-lint" version = "1.1.0" dependencies = [ - "auto_impl", - "clap", - "eyre", - "foundry-common", "foundry-compilers", "foundry-config", "heck", "rayon", - "serde", - "serde_json", "solar-ast", "solar-interface", "solar-parse", "thiserror 2.0.12", - "yansi", ] [[package]] diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index 2c78163606146..f81c252c40fdb 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -1,5 +1,5 @@ use clap::{Parser, ValueHint}; -use eyre::Result; +use eyre::{eyre, Result}; use forge_lint::{ linter::Linter, sol::{SolLint, SolLintError, SolidityLinter}, @@ -101,18 +101,18 @@ impl LintArgs { None => config.lint.severity, }; - if project.compiler.solc.is_some() { - let linter = SolidityLinter::new() - .with_json_emitter(self.json) - .with_description(true) - .with_lints(include) - .without_lints(exclude) - .with_severity(if severity.is_empty() { None } else { Some(severity) }); - - linter.lint(&input); - } else { - todo!("Linting not supported for this language"); - }; + if project.compiler.solc.is_none() { + return Err(eyre!("Linting not supported for this language")); + } + + let linter = SolidityLinter::new() + .with_json_emitter(self.json) + .with_description(true) + .with_lints(include) + .without_lints(exclude) + .with_severity(if severity.is_empty() { None } else { Some(severity) }); + + linter.lint(&input); Ok(()) } diff --git a/crates/forge/tests/ui.rs b/crates/forge/tests/ui.rs index bb95a4430c1fd..0f1ec553c4c75 100644 --- a/crates/forge/tests/ui.rs +++ b/crates/forge/tests/ui.rs @@ -1,5 +1,5 @@ -use foundry_test_utils::runner; -use std::path::Path; +use foundry_test_utils::ui_runner; +use std::{env, path::Path}; const FORGE_CMD: &str = env!("CARGO_BIN_EXE_forge"); const FORGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); @@ -9,6 +9,7 @@ fn forge_lint_ui_tests() -> eyre::Result<()> { let forge_cmd = Path::new(FORGE_CMD); let forge_dir = Path::new(FORGE_DIR); let lint_testdata = forge_dir.parent().unwrap().join("lint").join("testdata"); + let bless = env::var("BLESS").is_ok(); - runner::run_tests("lint", forge_cmd, &lint_testdata, true) + ui_runner::run_tests("lint", forge_cmd, &lint_testdata, bless) } diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index b5568847aed71..a621caa713d07 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -15,7 +15,6 @@ workspace = true [dependencies] # lib -foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true @@ -24,11 +23,5 @@ solar-ast.workspace = true solar-interface = { workspace = true, features = ["json"] } heck.workspace = true -eyre.workspace = true rayon.workspace = true thiserror.workspace = true -serde_json.workspace = true -auto_impl.workspace = true -yansi.workspace = true -serde = { workspace = true, features = ["derive"] } -clap = { version = "4", features = ["derive"] } diff --git a/crates/lint/README.md b/crates/lint/README.md new file mode 100644 index 0000000000000..32f0579385e0c --- /dev/null +++ b/crates/lint/README.md @@ -0,0 +1,120 @@ +# Linter (`lint`) + +Solidity linter for identifying potential errors, vulnerabilities, gas optimizations, and style guide violations. +It helps enforce best practices and improve code quality within Foundry projects. + +## Supported Lints + +`forge-lint` includes rules across several categories: + +* **High Severity:** + * `incorrect-shift`: Warns against shift operations where operands might be in the wrong order. +* **Medium Severity:** + * `divide-before-multiply`: Warns against performing division before multiplication in the same expression, which can cause precision loss. +* **Informational / Style Guide:** + * `pascal-case-struct`: Flags for struct names not adhering to `PascalCase`. + * `mixed-case-function`: Flags for function names not adhering to `mixedCase`. + * `mixed-case-variable`: Flags for mutable variable names not adhering to `mixedCase`. + * `screaming-snake-case-const`: Flags for `constant` variable names not adhering to `SCREAMING_SNAKE_CASE`. + * `screaming-snake-case-immutable`: Flags for `immutable` variable names not adhering to `SCREAMING_SNAKE_CASE`. +* **Gas Optimizations:** + * `asm-keccak256`: Recommends using inline assembly for `keccak256` for potential gas savings. + +## Architecture + +The `forge-lint` system operates by analyzing Solidity source code: + +1. **Parsing**: Solidity source files are parsed into an Abstract Syntax Tree (AST) using `solar-parse`. This AST represents the structure of the code. +2. **AST Traversal**: The generated AST is then traversed using a Visitor pattern. The `EarlyLintVisitor` is responsible for walking through the AST nodes. +3. **Applying Lint Passes**: As the visitor encounters different AST nodes (like functions, expressions, variable definitions), it invokes registered "lint passes" (`EarlyLintPass` implementations). Each pass is designed to check for a specific code pattern. +4. **Emitting Diagnostics**: If a lint pass identifies a violation of its rule, it uses the `LintContext` to emit a diagnostic (either `warning` or `note`) that pinpoints the issue in the source code. + +### Key Components + +* **`Linter` Trait**: Defines a generic interface for linters. `SolidityLinter` is the concrete implementation tailored for Solidity. +* **`Lint` Trait & `SolLint` Struct**: + * `Lint`: A trait that defines the essential properties of a lint rule, such as its unique ID, severity, description, and an optional help message/URL. + * `SolLint`: A struct implementing the `Lint` trait, used to hold the metadata for each specific Solidity lint rule. +* **`EarlyLintPass<'ast>` Trait**: Lints that operate directly on AST nodes implement this trait. It contains methods (like `check_expr`, `check_item_function`, etc.) called by the visitor. +* **`LintContext<'s>`**: Provides contextual information to lint passes during execution, such as access to the session for emitting diagnostics. +* **`EarlyLintVisitor<'a, 's, 'ast>`**: The core visitor that traverses the AST and dispatches checks to the registered `EarlyLintPass` instances. + +## Configuration + +The behavior of the `SolidityLinter` can be customized with the following options: + +| Option | Default | Description | +|---------------------|---------|------------------------------------------------------------------------------------------------------------| +| `with_severity` | `None` | Filters active lints by their severity (`High`, `Med`, `Low`, `Info`, `Gas`). `None` means all severities. | +| `with_lints` | `None` | Specifies a list of `SolLint` instances to include. Overrides severity filter if a lint matches. | +| `without_lints` | `None` | Specifies a list of `SolLint` instances to exclude, even if they match other criteria. | +| `with_description` | `true` | Whether to include the lint's description in the diagnostic output. | +| `with_json_emitter` | `false` | If `true`, diagnostics are output in rustc-compatible JSON format; otherwise, human-readable text. | + +## Contributing + +Check out the [foundry contribution guide](https://github.com/foundry-rs/foundry/blob/master/CONTRIBUTING.md). + +Guidelines for contributing to `forge lint`: + +### Opening an issue + +1. Create a short concise title describing an issue. + - Bad Title Examples + ```text + Forge lint does not work + Forge lint breaks + Forge lint unexpected behavior + ``` + - Good Title Examples + ```text + Forge lint does not flag incorrect shift operations + ``` +2. Fill in the issue template fields that include foundry version, platform & component info. +3. Provide the code snippets showing the current & expected behaviors. +4. If it's a feature request, specify why this feature is needed. +5. Besides the default label (`T-Bug` for bugs or `T-feature` for features), add `C-forge` and `Cmd-forge-fmt` labels. + +### Fixing A Bug + +1. Specify an issue that is being addressed in the PR description. +2. Add a note on the solution in the PR description. +3. Add a test case to `lint/testdata` that specifically demonstrates the bug and is fixed by your changes. Ensure all tests pass. + +### Developing a New Lint Rule + +1. Specify an issue that is being addressed in the PR description. +2. In your PR: + * Implement the lint logic by creating a new struct and implementing the `EarlyLintPass` trait for it within the relevant severity module (e.g., `src/sol/med/my_new_lint.rs`). + * Declare your `SolLint` metadata using `declare_forge_lint!`. + * Register your pass and lint using `register_lints!` in the `mod.rs` of its severity category. +3. Add comprehensive tests in `lint/testdata/`: + * Create `MyNewLint.sol` with various examples (triggering and non-triggering cases, edge cases). + * Create `MyNewLint.stderr` with the expected output. + +### Testing + +Tests are located in the `lint/testdata` directory. A test for a lint rule involves: + + - A Solidity source file with various code snippets, some of which are expected to trigger the lint. Expected diagnostics must be indicated with either `//~WARN: description` or `//~NOTE: description` on the relevant line. + - corresponding `.stderr` (blessed) file which contains the exact diagnostic output the linter is expected to produce for that source file. + +The testing framework runs the linter on the `.sol` file and compares its standard error output against the content of the `.stderr` file to ensure correctness. + +- Run the following commands to trigger the ui test runner: + ```sh + // using the default cargo cmd for running tests + cargo test -p forge --test ui + + // using `nextest` for running tests + cargo nextest run -p forge --test ui + ``` + +- If you need to generate the blessed files: + ```sh + // using the default cargo cmd for running tests + BLESS=1 cargo test -p forge --test ui + + // using `nextest` for running tests + BLESS=1 cargo nextest run -p forge --test ui + ``` diff --git a/crates/lint/src/lib.rs b/crates/lint/src/lib.rs index e10498bdf4de7..5dd1d9d10b209 100644 --- a/crates/lint/src/lib.rs +++ b/crates/lint/src/lib.rs @@ -1,6 +1,6 @@ -//! # forge-lint -//! -//! Types, traits, and utilities for linting Solidity projects. +#![doc = include_str!("../README.md")] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] pub mod linter; pub mod sol; diff --git a/crates/lint/src/sol/gas/keccack.rs b/crates/lint/src/sol/gas/keccak.rs similarity index 88% rename from crates/lint/src/sol/gas/keccack.rs rename to crates/lint/src/sol/gas/keccak.rs index 94a7859b6dfc1..68ffeb8da264c 100644 --- a/crates/lint/src/sol/gas/keccack.rs +++ b/crates/lint/src/sol/gas/keccak.rs @@ -1,5 +1,5 @@ use solar_ast::{Expr, ExprKind}; -use solar_interface::kw::Keccak256; +use solar_interface::kw; use super::AsmKeccak256; use crate::{ @@ -20,7 +20,7 @@ impl<'ast> EarlyLintPass<'ast> for AsmKeccak256 { fn check_expr(&mut self, ctx: &crate::linter::LintContext<'_>, expr: &'ast Expr<'ast>) { if let ExprKind::Call(expr, _) = &expr.kind { if let ExprKind::Ident(ident) = &expr.kind { - if ident.name == Keccak256 { + if ident.name == kw::Keccak256 { ctx.emit(&ASM_KECCAK256, expr.span); } } diff --git a/crates/lint/src/sol/gas/mod.rs b/crates/lint/src/sol/gas/mod.rs index 4f7545feb2cab..7c25c5b771eba 100644 --- a/crates/lint/src/sol/gas/mod.rs +++ b/crates/lint/src/sol/gas/mod.rs @@ -1,5 +1,5 @@ -mod keccack; -use keccack::ASM_KECCAK256; +mod keccak; +use keccak::ASM_KECCAK256; use crate::{ register_lints, diff --git a/crates/lint/src/sol/info/screaming_snake_case.rs b/crates/lint/src/sol/info/screaming_snake_case.rs index d98af132a7ac1..6703cea425b2c 100644 --- a/crates/lint/src/sol/info/screaming_snake_case.rs +++ b/crates/lint/src/sol/info/screaming_snake_case.rs @@ -1,4 +1,4 @@ -use solar_ast::{Span, VarMut, VariableDefinition}; +use solar_ast::{VarMut, VariableDefinition}; use super::ScreamingSnakeCase; use crate::{ @@ -29,27 +29,19 @@ impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { var: &'ast VariableDefinition<'ast>, ) { if let (Some(name), Some(mutability)) = (var.name, var.mutability) { - let name = name.as_str(); - if name.len() < 2 || is_screaming_snake_case(name) { + let name_str = name.as_str(); + if name_str.len() < 2 || is_screaming_snake_case(name_str) { return; } match mutability { - VarMut::Constant => ctx.emit(&SCREAMING_SNAKE_CASE_CONSTANT, get_var_span(var)), - VarMut::Immutable => ctx.emit(&SCREAMING_SNAKE_CASE_IMMUTABLE, get_var_span(var)), + VarMut::Constant => ctx.emit(&SCREAMING_SNAKE_CASE_CONSTANT, name.span), + VarMut::Immutable => ctx.emit(&SCREAMING_SNAKE_CASE_IMMUTABLE, name.span), } } } } -/// Get the variable name span if available. Otherwise default to the line span. -fn get_var_span<'ast>(var: &'ast VariableDefinition<'ast>) -> Span { - match var.name { - Some(ident) => ident.span, - None => var.span, - } -} - /// Check if a string is SCREAMING_SNAKE_CASE. Numbers don't need to be preceded by an underscore. pub fn is_screaming_snake_case(s: &str) -> bool { if s.len() <= 1 { diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 9f646d6765fec..4e326b10fb85b 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -30,8 +30,7 @@ pub use util::{TestCommand, TestProject}; mod script; pub use script::{ScriptOutcome, ScriptTester}; -// UI test runner -pub mod runner; +pub mod ui_runner; // re-exports for convenience pub use foundry_compilers; diff --git a/crates/test-utils/src/runner.rs b/crates/test-utils/src/ui_runner.rs similarity index 98% rename from crates/test-utils/src/runner.rs rename to crates/test-utils/src/ui_runner.rs index 2f575d1d05502..9b02e70e16f05 100644 --- a/crates/test-utils/src/runner.rs +++ b/crates/test-utils/src/ui_runner.rs @@ -78,7 +78,7 @@ fn config<'a>( cfg_flag: None, }, output_conflict_handling: ui_test::error_on_output_conflict, - bless_command: Some("cargo uibless".into()), + bless_command: Some(format!("BLESS=1 cargo nextest run {}", module_path!())), out_dir: root.join("target/ui"), comment_start: "//", diagnostic_extractor: ui_test::diagnostics::rustc::rustc_diagnostics_extractor, From 11e25a815cd519848f48f67df18e0b1fa6aac6d3 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 8 May 2025 18:15:02 +0200 Subject: [PATCH 094/107] docs: linter docs for users + devs --- crates/forge/Cargo.toml | 5 ++ crates/forge/tests/ui.rs | 6 +-- crates/lint/README.md | 55 +-------------------- crates/test-utils/src/ui_runner.rs | 10 +--- docs/dev/lintrules.md | 78 ++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 66 deletions(-) create mode 100644 docs/dev/lintrules.md diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 9f7ab5fa2d354..e9140d0f4c679 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -17,6 +17,11 @@ workspace = true name = "forge" path = "bin/main.rs" +[[test]] +name = "ui" +path = "tests/ui.rs" +harness = false + [dependencies] # lib foundry-block-explorers = { workspace = true, features = ["foundry-compilers"] } diff --git a/crates/forge/tests/ui.rs b/crates/forge/tests/ui.rs index 0f1ec553c4c75..9b37b7b1f904d 100644 --- a/crates/forge/tests/ui.rs +++ b/crates/forge/tests/ui.rs @@ -4,12 +4,10 @@ use std::{env, path::Path}; const FORGE_CMD: &str = env!("CARGO_BIN_EXE_forge"); const FORGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); -#[test] -fn forge_lint_ui_tests() -> eyre::Result<()> { +fn main() -> impl std::process::Termination { let forge_cmd = Path::new(FORGE_CMD); let forge_dir = Path::new(FORGE_DIR); let lint_testdata = forge_dir.parent().unwrap().join("lint").join("testdata"); - let bless = env::var("BLESS").is_ok(); - ui_runner::run_tests("lint", forge_cmd, &lint_testdata, bless) + ui_runner::run_tests("lint", forge_cmd, &lint_testdata) } diff --git a/crates/lint/README.md b/crates/lint/README.md index 32f0579385e0c..e8200706ff7b9 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -20,25 +20,6 @@ It helps enforce best practices and improve code quality within Foundry projects * **Gas Optimizations:** * `asm-keccak256`: Recommends using inline assembly for `keccak256` for potential gas savings. -## Architecture - -The `forge-lint` system operates by analyzing Solidity source code: - -1. **Parsing**: Solidity source files are parsed into an Abstract Syntax Tree (AST) using `solar-parse`. This AST represents the structure of the code. -2. **AST Traversal**: The generated AST is then traversed using a Visitor pattern. The `EarlyLintVisitor` is responsible for walking through the AST nodes. -3. **Applying Lint Passes**: As the visitor encounters different AST nodes (like functions, expressions, variable definitions), it invokes registered "lint passes" (`EarlyLintPass` implementations). Each pass is designed to check for a specific code pattern. -4. **Emitting Diagnostics**: If a lint pass identifies a violation of its rule, it uses the `LintContext` to emit a diagnostic (either `warning` or `note`) that pinpoints the issue in the source code. - -### Key Components - -* **`Linter` Trait**: Defines a generic interface for linters. `SolidityLinter` is the concrete implementation tailored for Solidity. -* **`Lint` Trait & `SolLint` Struct**: - * `Lint`: A trait that defines the essential properties of a lint rule, such as its unique ID, severity, description, and an optional help message/URL. - * `SolLint`: A struct implementing the `Lint` trait, used to hold the metadata for each specific Solidity lint rule. -* **`EarlyLintPass<'ast>` Trait**: Lints that operate directly on AST nodes implement this trait. It contains methods (like `check_expr`, `check_item_function`, etc.) called by the visitor. -* **`LintContext<'s>`**: Provides contextual information to lint passes during execution, such as access to the session for emitting diagnostics. -* **`EarlyLintVisitor<'a, 's, 'ast>`**: The core visitor that traverses the AST and dispatches checks to the registered `EarlyLintPass` instances. - ## Configuration The behavior of the `SolidityLinter` can be customized with the following options: @@ -83,38 +64,4 @@ Guidelines for contributing to `forge lint`: ### Developing a New Lint Rule -1. Specify an issue that is being addressed in the PR description. -2. In your PR: - * Implement the lint logic by creating a new struct and implementing the `EarlyLintPass` trait for it within the relevant severity module (e.g., `src/sol/med/my_new_lint.rs`). - * Declare your `SolLint` metadata using `declare_forge_lint!`. - * Register your pass and lint using `register_lints!` in the `mod.rs` of its severity category. -3. Add comprehensive tests in `lint/testdata/`: - * Create `MyNewLint.sol` with various examples (triggering and non-triggering cases, edge cases). - * Create `MyNewLint.stderr` with the expected output. - -### Testing - -Tests are located in the `lint/testdata` directory. A test for a lint rule involves: - - - A Solidity source file with various code snippets, some of which are expected to trigger the lint. Expected diagnostics must be indicated with either `//~WARN: description` or `//~NOTE: description` on the relevant line. - - corresponding `.stderr` (blessed) file which contains the exact diagnostic output the linter is expected to produce for that source file. - -The testing framework runs the linter on the `.sol` file and compares its standard error output against the content of the `.stderr` file to ensure correctness. - -- Run the following commands to trigger the ui test runner: - ```sh - // using the default cargo cmd for running tests - cargo test -p forge --test ui - - // using `nextest` for running tests - cargo nextest run -p forge --test ui - ``` - -- If you need to generate the blessed files: - ```sh - // using the default cargo cmd for running tests - BLESS=1 cargo test -p forge --test ui - - // using `nextest` for running tests - BLESS=1 cargo nextest run -p forge --test ui - ``` +Check the [dev docs](../../docs/dev/lintrules.md) for a full implementation guide. diff --git a/crates/test-utils/src/ui_runner.rs b/crates/test-utils/src/ui_runner.rs index 9b02e70e16f05..b7483d4982c37 100644 --- a/crates/test-utils/src/ui_runner.rs +++ b/crates/test-utils/src/ui_runner.rs @@ -2,16 +2,10 @@ use std::path::Path; use ui_test::spanned::Spanned; /// Test runner based on `ui_test`. Adapted from `https://github.com/paradigmxyz/solar/tools/tester`. -pub fn run_tests<'a>( - cmd: &str, - cmd_path: &'a Path, - testdata: &'a Path, - bless: bool, -) -> eyre::Result<()> { +pub fn run_tests<'a>(cmd: &str, cmd_path: &'a Path, testdata: &'a Path) -> eyre::Result<()> { ui_test::color_eyre::install()?; let mut args = ui_test::Args::test()?; - args.bless = bless; // Fast path for `--list`, invoked by `cargo-nextest`. { @@ -78,7 +72,7 @@ fn config<'a>( cfg_flag: None, }, output_conflict_handling: ui_test::error_on_output_conflict, - bless_command: Some(format!("BLESS=1 cargo nextest run {}", module_path!())), + bless_command: Some(format!("cargo nextest run {} -- --bless", module_path!())), out_dir: root.join("target/ui"), comment_start: "//", diagnostic_extractor: ui_test::diagnostics::rustc::rustc_diagnostics_extractor, diff --git a/docs/dev/lintrules.md b/docs/dev/lintrules.md new file mode 100644 index 0000000000000..eedd7d9432bc3 --- /dev/null +++ b/docs/dev/lintrules.md @@ -0,0 +1,78 @@ +# Linter (`lint`) + +Solidity linter for identifying potential errors, vulnerabilities, gas optimizations, and style guide violations. +It helps enforce best practices and improve code quality within Foundry projects. + +## Architecture + +The `forge-lint` system operates by analyzing Solidity source code: + +1. **Parsing**: Solidity source files are parsed into an Abstract Syntax Tree (AST) using `solar-parse`. This AST represents the structure of the code. +2. **AST Traversal**: The generated AST is then traversed using a Visitor pattern. The `EarlyLintVisitor` is responsible for walking through the AST nodes. +3. **Applying Lint Passes**: As the visitor encounters different AST nodes (like functions, expressions, variable definitions), it invokes registered "lint passes" (`EarlyLintPass` implementations). Each pass is designed to check for a specific code pattern. +4. **Emitting Diagnostics**: If a lint pass identifies a violation of its rule, it uses the `LintContext` to emit a diagnostic (either `warning` or `note`) that pinpoints the issue in the source code. + +### Key Components + +* **`Linter` Trait**: Defines a generic interface for linters. `SolidityLinter` is the concrete implementation tailored for Solidity. +* **`Lint` Trait & `SolLint` Struct**: + * `Lint`: A trait that defines the essential properties of a lint rule, such as its unique ID, severity, description, and an optional help message/URL. + * `SolLint`: A struct implementing the `Lint` trait, used to hold the metadata for each specific Solidity lint rule. +* **`EarlyLintPass<'ast>` Trait**: Lints that operate directly on AST nodes implement this trait. It contains methods (like `check_expr`, `check_item_function`, etc.) called by the visitor. +* **`LintContext<'s>`**: Provides contextual information to lint passes during execution, such as access to the session for emitting diagnostics. +* **`EarlyLintVisitor<'a, 's, 'ast>`**: The core visitor that traverses the AST and dispatches checks to the registered `EarlyLintPass` instances. + +## Developing a new lint rule + +1. Specify an issue that is being addressed in the PR description. +2. In your PR: + * Create a static `SolLint` instance using the `declare_forge_lint!` to define its metadata. + ```rust + declare_forge_lint!( + MIXED_CASE_FUNCTION, // The Rust identifier for this SolLint static + Severity::Info, // The default severity of the lint + "mixed-case-function", // A unique string ID for configuration/CLI + "function names should use mixedCase", // A brief description + "https://docs.soliditylang.org/en/latest/style-guide.html#function-names" // Optional help link + ); + ``` + + * Register the pass struct and the lint using `register_lints!` in the `mod.rs` of its corresponding severity category. This macro generates the necessary boilerplate to make them discoverable by the linter, and creates helper functions to instantiate them. + ```rust + register_lints!( + (PascalCaseStruct, (PASCAL_CASE_STRUCT)), + (MixedCaseVariable, (MIXED_CASE_VARIABLE)), + (MixedCaseFunction, (MIXED_CASE_FUNCTION)) + ); + // The structs `PascalCaseStruct`, `MixedCaseVariable`, etc., would have to manually implement `EarlyLintPass`. + ``` + + * Implement the `EarlyLintPass` trait logic for the pass struct. Do it in a new file within the relevant severity module (e.g., `src/sol/med/my_new_lint.rs`). + +3. Add comprehensive tests in `lint/testdata/`: + * Create `MyNewLint.sol` with various examples (triggering and non-triggering cases, edge cases). + * Generate the corresponding blessed file with the expected output. + +### Testing a lint rule + +Tests are located in the `lint/testdata/` directory. A test for a lint rule involves: + + - A Solidity source file with various code snippets, some of which are expected to trigger the lint. Expected diagnostics must be indicated with either `//~WARN: description` or `//~NOTE: description` on the relevant line. + - corresponding `.stderr` (blessed) file which contains the exact diagnostic output the linter is expected to produce for that source file. + +The testing framework runs the linter on the `.sol` file and compares its standard error output against the content of the `.stderr` file to ensure correctness. + +- Run the following command to trigger the ui test runner: + ```sh + // using the default cargo cmd for running tests + cargo test -p forge --test ui + + // using nextest + cargo nextest run -p forge test ui + ``` + +- If you need to generate the blessed files: + ```sh + // using the default cargo cmd for running tests + cargo test -p forge --test ui -- --bless + ``` From dbb28b7448bfaa50331187f3cf0065d03fa713ee Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 8 May 2025 18:23:45 +0200 Subject: [PATCH 095/107] docs: style --- docs/dev/lintrules.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dev/lintrules.md b/docs/dev/lintrules.md index eedd7d9432bc3..0a0f81fe325a9 100644 --- a/docs/dev/lintrules.md +++ b/docs/dev/lintrules.md @@ -29,10 +29,10 @@ The `forge-lint` system operates by analyzing Solidity source code: * Create a static `SolLint` instance using the `declare_forge_lint!` to define its metadata. ```rust declare_forge_lint!( - MIXED_CASE_FUNCTION, // The Rust identifier for this SolLint static - Severity::Info, // The default severity of the lint - "mixed-case-function", // A unique string ID for configuration/CLI - "function names should use mixedCase", // A brief description + MIXED_CASE_FUNCTION, // The Rust identifier for this SolLint static + Severity::Info, // The default severity of the lint + "mixed-case-function", // A unique string ID for configuration/CLI + "function names should use mixedCase", // A brief description "https://docs.soliditylang.org/en/latest/style-guide.html#function-names" // Optional help link ); ``` From 78320e91e48ced7b9ea15acd54dd018cdeb03e22 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 8 May 2025 18:24:02 +0200 Subject: [PATCH 096/107] docs: style --- crates/lint/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/README.md b/crates/lint/README.md index e8200706ff7b9..aae8ae0e2ab5c 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -60,7 +60,7 @@ Guidelines for contributing to `forge lint`: 1. Specify an issue that is being addressed in the PR description. 2. Add a note on the solution in the PR description. -3. Add a test case to `lint/testdata` that specifically demonstrates the bug and is fixed by your changes. Ensure all tests pass. +3. Add a test case to `lint/testdata` that specifically demonstrates the bug and is fixed by your changes. Ensure all tests pass. ### Developing a New Lint Rule From 8ee47c281dda875dffa9d332a1643fd4111dcce9 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Mon, 12 May 2025 17:18:51 +0300 Subject: [PATCH 097/107] clone lint testdata with lf --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 0e0276a958df8..a1451e6b491a5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,4 @@ testdata/cheats/Vm.sol linguist-generated # See *.rs diff=rust +crates/lint/testdata/* text eol=lf From 43126155bb04924f347465c65ac203b75451add3 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Mon, 12 May 2025 16:54:04 +0200 Subject: [PATCH 098/107] fix: out dir constructor --- crates/test-utils/src/ui_runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/test-utils/src/ui_runner.rs b/crates/test-utils/src/ui_runner.rs index b7483d4982c37..9f6b87a8055ce 100644 --- a/crates/test-utils/src/ui_runner.rs +++ b/crates/test-utils/src/ui_runner.rs @@ -73,7 +73,7 @@ fn config<'a>( }, output_conflict_handling: ui_test::error_on_output_conflict, bless_command: Some(format!("cargo nextest run {} -- --bless", module_path!())), - out_dir: root.join("target/ui"), + out_dir: root.join("target").join("ui"), comment_start: "//", diagnostic_extractor: ui_test::diagnostics::rustc::rustc_diagnostics_extractor, ..ui_test::Config::dummy() From 3a74197d2f0923c8a60c18e5f9b959e892a3a164 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 19 May 2025 17:47:47 +0200 Subject: [PATCH 099/107] update toml --- Cargo.lock | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ece12fa473aa..9284a53fb2165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,7 +343,7 @@ dependencies = [ "const-hex", "derive_more 2.0.1", "foldhash", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "indexmap 2.9.0", "itoa", "k256", @@ -771,7 +771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "251273c5aa1abb590852f795c938730fa641832fc8fa77b5478ed1bf11b6097e" dependencies = [ "serde", - "winnow 0.7.7", + "winnow 0.7.9", ] [[package]] @@ -2543,7 +2543,7 @@ dependencies = [ "eyre", "indenter", "once_cell", - "owo-colors", + "owo-colors 4.2.0", "tracing-error", ] @@ -2554,7 +2554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ddd8d5bfda1e11a501d0a7303f3bfed9aa632ebdb859be40d0fd70478ed70d5" dependencies = [ "once_cell", - "owo-colors", + "owo-colors 4.2.0", "tracing-core", "tracing-error", ] @@ -6435,6 +6435,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "owo-colors" version = "4.2.0" @@ -6851,7 +6857,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abec3fb083c10660b3854367697da94c674e9e82aa7511014dc958beeb7215e9" dependencies = [ - "owo-colors", + "owo-colors 3.5.0", "pad", ] From 7c8532ef6d6733a34d78c6cf834b07bbddc87ad4 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 19 May 2025 19:56:41 +0200 Subject: [PATCH 100/107] fix: merge conflicts --- crates/anvil/core/src/eth/transaction/mod.rs | 2 +- crates/cast/src/args.rs | 2 +- crates/cheatcodes/src/error.rs | 1 + crates/common/src/transactions.rs | 34 ++++++++++---------- crates/evm/core/src/fork/multi.rs | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/anvil/core/src/eth/transaction/mod.rs b/crates/anvil/core/src/eth/transaction/mod.rs index b9fa0192cd39e..6f047a7f13e5e 100644 --- a/crates/anvil/core/src/eth/transaction/mod.rs +++ b/crates/anvil/core/src/eth/transaction/mod.rs @@ -1718,7 +1718,7 @@ mod tests { fn deser_to_type_tx() { let tx = r#" { - "EIP1559": { + "EIP1559": { "chainId": "0x7a69", "nonce": "0x0", "gas": "0x5209", diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 36c8d4c89989d..fb5ee336235c6 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -3,7 +3,7 @@ use crate::{ traces::identifier::SignaturesIdentifier, Cast, SimpleCast, }; -use alloy_consensus::transaction::Recovered; +use alloy_consensus::transaction::{Recovered, SignerRecoverable}; use alloy_dyn_abi::{DynSolValue, ErrorExt, EventExt}; use alloy_primitives::{eip191_hash_message, hex, keccak256, Address, B256}; use alloy_provider::Provider; diff --git a/crates/cheatcodes/src/error.rs b/crates/cheatcodes/src/error.rs index 4bb9f1395bf2b..414c7dbed8f90 100644 --- a/crates/cheatcodes/src/error.rs +++ b/crates/cheatcodes/src/error.rs @@ -283,6 +283,7 @@ impl_from!( alloy_sol_types::Error, alloy_dyn_abi::Error, alloy_primitives::SignatureError, + alloy_consensus::crypto::RecoveryError, eyre::Report, FsPathError, hex::FromHexError, diff --git a/crates/common/src/transactions.rs b/crates/common/src/transactions.rs index a3ef9c4d8a642..0933764b5c6d1 100644 --- a/crates/common/src/transactions.rs +++ b/crates/common/src/transactions.rs @@ -1,6 +1,6 @@ //! Wrappers for transactions. -use alloy_consensus::{Transaction, TxEnvelope}; +use alloy_consensus::{transaction::SignerRecoverable, Transaction, TxEnvelope}; use alloy_eips::eip7702::SignedAuthorization; use alloy_network::AnyTransactionReceipt; use alloy_primitives::{Address, TxKind, U256}; @@ -175,20 +175,6 @@ pub fn get_pretty_tx_receipt_attr( } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_revert_reason() { - let error_string_1 = "server returned an error response: error code 3: execution reverted: Transaction too old"; - let error_string_2 = "server returned an error response: error code 3: Invalid signature"; - - assert_eq!(extract_revert_reason(error_string_1), Some("Transaction too old".to_string())); - assert_eq!(extract_revert_reason(error_string_2), None); - } -} - /// Used for broadcasting transactions /// A transaction can either be a [`TransactionRequest`] waiting to be signed /// or a [`TxEnvelope`], already signed @@ -212,7 +198,7 @@ impl TransactionMaybeSigned { /// Creates a new signed transaction for broadcast. pub fn new_signed( tx: TxEnvelope, - ) -> core::result::Result { + ) -> core::result::Result { let from = tx.recover_signer()?; Ok(Self::Signed { tx, from }) } @@ -286,9 +272,23 @@ impl From for TransactionMaybeSigned { } impl TryFrom for TransactionMaybeSigned { - type Error = alloy_primitives::SignatureError; + type Error = alloy_consensus::crypto::RecoveryError; fn try_from(tx: TxEnvelope) -> core::result::Result { Self::new_signed(tx) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_revert_reason() { + let error_string_1 = "server returned an error response: error code 3: execution reverted: Transaction too old"; + let error_string_2 = "server returned an error response: error code 3: Invalid signature"; + + assert_eq!(extract_revert_reason(error_string_1), Some("Transaction too old".to_string())); + assert_eq!(extract_revert_reason(error_string_2), None); + } +} diff --git a/crates/evm/core/src/fork/multi.rs b/crates/evm/core/src/fork/multi.rs index 59a05d67be2f2..f68197a08ee46 100644 --- a/crates/evm/core/src/fork/multi.rs +++ b/crates/evm/core/src/fork/multi.rs @@ -314,7 +314,7 @@ impl MultiForkHandler { Request::CreateFork(fork, sender) => self.create_fork(*fork, sender), Request::GetFork(fork_id, sender) => { let fork = self.forks.get(&fork_id).map(|f| f.backend.clone()); - let _ = sender.send(fork); + let _ = sender.send(fork.into()); } Request::RollFork(fork_id, block, sender) => { if let Some(fork) = self.forks.get(&fork_id) { From 4e8094a67d742fd1e1455c7959aaecf8a1124c73 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 19 May 2025 20:14:06 +0200 Subject: [PATCH 101/107] fix: cargo.lock merge conflicts --- Cargo.lock | 364 ++--------------------------------------------------- 1 file changed, 7 insertions(+), 357 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55dd8211b3261..bfee75cf92bd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3789,16 +3789,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "faster-hex" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" -dependencies = [ - "heapless", - "serde", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -3911,7 +3901,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", ] @@ -5034,248 +5023,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gix-actor" -version = "0.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b300e6e4f31f3f6bd2de5e2b0caab192ced00dc0fcd0f7cc56e28c575c8e1ff" -dependencies = [ - "bstr", - "gix-date", - "gix-utils", - "itoa", - "thiserror 2.0.12", - "winnow", -] - -[[package]] -name = "gix-config" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f3c8f357ae049bfb77493c2ec9010f58cfc924ae485e1116c3718fc0f0d881" -dependencies = [ - "bstr", - "gix-config-value", - "gix-features", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "memchr", - "once_cell", - "smallvec", - "thiserror 2.0.12", - "unicode-bom", - "winnow", -] - -[[package]] -name = "gix-config-value" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439d62e241dae2dffd55bfeeabe551275cf9d9f084c5ebc6b48bad49d03285b7" -dependencies = [ - "bitflags 2.9.1", - "bstr", - "gix-path", - "libc", - "thiserror 2.0.12", -] - -[[package]] -name = "gix-date" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139d1d52b21741e3f0c72b0fc65e1ff34d4eaceb100ef529d182725d2e09b8cb" -dependencies = [ - "bstr", - "itoa", - "jiff", - "smallvec", - "thiserror 2.0.12", -] - -[[package]] -name = "gix-features" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" -dependencies = [ - "gix-path", - "gix-trace", - "gix-utils", - "libc", - "prodash", - "walkdir", -] - -[[package]] -name = "gix-fs" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a0637149b4ef24d3ea55f81f77231401c8463fae6da27331c987957eb597c7" -dependencies = [ - "bstr", - "fastrand", - "gix-features", - "gix-path", - "gix-utils", - "thiserror 2.0.12", -] - -[[package]] -name = "gix-glob" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90181472925b587f6079698f79065ff64786e6d6c14089517a1972bca99fb6e9" -dependencies = [ - "bitflags 2.9.1", - "bstr", - "gix-features", - "gix-path", -] - -[[package]] -name = "gix-hash" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8" -dependencies = [ - "faster-hex", - "gix-features", - "sha1-checked", - "thiserror 2.0.12", -] - -[[package]] -name = "gix-hashtable" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b5cb3c308b4144f2612ff64e32130e641279fcf1a84d8d40dad843b4f64904" -dependencies = [ - "gix-hash", - "hashbrown 0.14.5", - "parking_lot", -] - -[[package]] -name = "gix-lock" -version = "17.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "570f8b034659f256366dc90f1a24924902f20acccd6a15be96d44d1269e7a796" -dependencies = [ - "gix-tempfile", - "gix-utils", - "thiserror 2.0.12", -] - -[[package]] -name = "gix-object" -version = "0.49.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d957ca3640c555d48bb27f8278c67169fa1380ed94f6452c5590742524c40fbb" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-path", - "gix-utils", - "gix-validate", - "itoa", - "smallvec", - "thiserror 2.0.12", - "winnow", -] - -[[package]] -name = "gix-path" -version = "0.10.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567f65fec4ef10dfab97ae71f26a27fd4d7fe7b8e3f90c8a58551c41ff3fb65b" -dependencies = [ - "bstr", - "gix-trace", - "gix-validate", - "home", - "once_cell", - "thiserror 2.0.12", -] - -[[package]] -name = "gix-ref" -version = "0.52.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1b7985657029684d759f656b09abc3e2c73085596d5cdb494428823970a7762" -dependencies = [ - "gix-actor", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "memmap2", - "thiserror 2.0.12", - "winnow", -] - -[[package]] -name = "gix-sec" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dabbc78c759ecc006b970339394951b2c8e1e38a37b072c105b80b84c308fd" -dependencies = [ - "bitflags 2.9.1", - "gix-path", - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "gix-tempfile" -version = "17.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c750e8c008453a2dba67a2b0d928b7716e05da31173a3f5e351d5457ad4470aa" -dependencies = [ - "gix-fs", - "libc", - "once_cell", - "parking_lot", - "tempfile", -] - -[[package]] -name = "gix-trace" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" - -[[package]] -name = "gix-utils" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" -dependencies = [ - "fastrand", - "unicode-normalization", -] - -[[package]] -name = "gix-validate" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" -dependencies = [ - "bstr", - "thiserror 2.0.12", -] - [[package]] name = "glob" version = "0.3.2" @@ -5351,15 +5098,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -5388,16 +5126,6 @@ dependencies = [ "serde", ] -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - [[package]] name = "heck" version = "0.5.0" @@ -5709,25 +5437,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "ignore-files" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "834d78be07a00bd65bdf068027f6b0118ef98d3779e1629edb6571616e28f60d" -dependencies = [ - "dunce", - "futures", - "gix-config", - "ignore", - "miette", - "normalize-path", - "project-origins", - "radix_trie", - "thiserror 2.0.12", - "tokio", - "tracing", -] - [[package]] name = "impl-codec" version = "0.6.0" @@ -6257,15 +5966,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" -dependencies = [ - "zlib-rs", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -7507,27 +7207,6 @@ dependencies = [ "windows", ] -[[package]] -name = "prodash" -version = "29.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04bb108f648884c23b98a0e940ebc2c93c0c3b89f04dbaf7eb8256ce617d1bc" -dependencies = [ - "log", - "parking_lot", -] - -[[package]] -name = "project-origins" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42382141d102db809df94324b513c388b047ebc47926eec5417623b88781527" -dependencies = [ - "futures", - "tokio", - "tokio-stream", -] - [[package]] name = "proptest" version = "1.6.0" @@ -8826,16 +8505,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest 0.10.7", - "sha1", -] - [[package]] name = "sha2" version = "0.9.9" @@ -9224,7 +8893,7 @@ dependencies = [ "tokio", "toml_edit", "uuid 1.16.0", - "zip 2.4.2", + "zip", "zip-extract", ] @@ -9451,7 +9120,7 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "url", - "zip 2.4.2", + "zip", ] [[package]] @@ -11183,26 +10852,13 @@ checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" dependencies = [ "aes", "arbitrary", + "bzip2", + "constant_time_eq", "crc32fast", "crossbeam-utils", + "deflate64", "displaydoc", "flate2", - "indexmap 2.9.0", - "memchr", - "thiserror 2.0.12", - "zopfli", -] - -[[package]] -name = "zip" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" -dependencies = [ - "arbitrary", - "bzip2", - "crc32fast", - "flate2", "getrandom 0.3.3", "hmac", "indexmap 2.9.0", @@ -11225,16 +10881,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a8c9e90f27d1435088a7b540b6cc8ae6ee525d992a695f16012d2f365b3d3c" dependencies = [ "log", - "thiserror 2.0.12", - "zip 3.0.0", + "thiserror 1.0.69", + "zip", ] -[[package]] -name = "zlib-rs" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" - [[package]] name = "zopfli" version = "0.8.2" From 59672ad5c8e68916621eeaf6a4c5f7496f907e29 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 19 May 2025 20:41:51 +0200 Subject: [PATCH 102/107] style: clippy --- crates/evm/core/src/fork/multi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/evm/core/src/fork/multi.rs b/crates/evm/core/src/fork/multi.rs index f68197a08ee46..59a05d67be2f2 100644 --- a/crates/evm/core/src/fork/multi.rs +++ b/crates/evm/core/src/fork/multi.rs @@ -314,7 +314,7 @@ impl MultiForkHandler { Request::CreateFork(fork, sender) => self.create_fork(*fork, sender), Request::GetFork(fork_id, sender) => { let fork = self.forks.get(&fork_id).map(|f| f.backend.clone()); - let _ = sender.send(fork.into()); + let _ = sender.send(fork); } Request::RollFork(fork_id, block, sender) => { if let Some(fork) = self.forks.get(&fork_id) { From 4380058bdd5a0f0cfd40fe5e787fd99b09c382b3 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 22 May 2025 21:12:05 -0500 Subject: [PATCH 103/107] style: whitespace --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index be0dd74aa7845..ac9d02415e2f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,7 +197,6 @@ foundry-wallets = { path = "crates/wallets" } foundry-linking = { path = "crates/linking" } # solc & compilation utilities - foundry-block-explorers = { version = "0.17.0", default-features = false } foundry-compilers = { version = "0.16.1", default-features = false } foundry-fork-db = "0.14" From 6d785d35958f87fcbf4d962940c22b4c019fd04e Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 22 May 2025 21:40:57 -0500 Subject: [PATCH 104/107] fix: clippy --- crates/anvil/core/src/eth/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index a4d05ed29562f..02d18e209c34e 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -35,6 +35,7 @@ pub struct Params { /// Represents ethereum JSON-RPC API #[derive(Clone, Debug, serde::Deserialize)] #[serde(tag = "method", content = "params")] +#[expect(clippy::large_enum_variant)] pub enum EthRequest { #[serde(rename = "web3_clientVersion", with = "empty_params")] Web3ClientVersion(()), From 8d25585cc5cf1bca040ab6906a8111f4854ec5e9 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 23 May 2025 08:07:34 +0300 Subject: [PATCH 105/107] Cargo.lock update to weekly task --- Cargo.lock | 338 ++++++++++++++++++++++++++--------------------------- 1 file changed, 167 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44ab9452d4991..ea0b862ec1302 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,14 +70,14 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2123d190c823301be54e14a12ef10c00823d90047a140aa41650c26668d5b1" +checksum = "e9835a7b6216cb8118323581e58a18b1a5014fce55ce718635aaea7fa07bd700" dependencies = [ - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-primitives", "alloy-rlp", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-trie", "auto_impl", "c-kzg", @@ -94,23 +94,23 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b71e608e06b3d55169595c8ff39bd2177389508b0e91da8400b48d88d3afd" +checksum = "aec7fdaa4f0e4e1ca7e9271ca7887fdd467ca3b9e101582dc6c2bbd1645eae1c" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-primitives", "alloy-rlp", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "serde", ] [[package]] name = "alloy-contract" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210f50a648d28ee4266ff42523186efab61dd54d2ab2f8853fe38ad45d254756" +checksum = "9e810f27a4162190b50cdf4dabedee3ad9028772bd7e370fdfc0f63b8bc116d3" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -130,15 +130,16 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18cc14d832bc3331ca22a1c7819de1ede99f58f61a7d123952af7dde8de124a6" +checksum = "4f90b63261b7744642f6075ed17db6de118eecbe9516ea6c6ffd444b80180b75" dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-type-parser", "alloy-sol-types", "arbitrary", + "const-hex", "derive_arbitrary", "derive_more 2.0.1", "itoa", @@ -207,16 +208,16 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2126e39e1557f0e661e0a5a36748a0bf280490ca2c0a1d149a55d2c2c8675" +checksum = "90fc566136b705991072f8f79762525e14f0ca39c38d45b034944770cb6c6b67" dependencies = [ "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", "alloy-primitives", "alloy-rlp", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "auto_impl", "c-kzg", "derive_more 2.0.1", @@ -246,7 +247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c5b34c78c42525917b236e4135b1951ca183ede4004b594db0effee8bed169" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-hardforks", "alloy-primitives", "alloy-sol-types", @@ -260,13 +261,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cf47e07c0dbcb8f1fa2c903d9eb32b14fec3dc04e60e7fee29fc0d3799fba34" +checksum = "765c0124a3174f136171df8498e4700266774c9de1008a0b987766cf215d08f6" dependencies = [ - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-primitives", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-trie", "serde", ] @@ -286,9 +287,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ccaa79753d7bf15f06399ea76922afbfaf8d18bebed9e8fc452984b4a90dcc9" +checksum = "0068ae277f5ee3153a95eaea8ff10e188ed8ccde9b7f9926305415a2c0ab2442" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -298,9 +299,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83498cbeb8353b78985ad6c127fc7376767c5d341e05e3f641076d61f3a78471" +checksum = "1590f44abdfe98686827a4e083b711ad17f843bf6ed8a50b78d3242f12a00ada" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -312,19 +313,19 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c443287af072731c28fb6c09b62fb882257a4d20fc151bc110357a56fb19559" +checksum = "049a9022caa0c0a2dcd2bc2ea23fa098508f4a81d5dda774d753570a41e6acdb" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-any", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-signer", "alloy-sol-types", "async-trait", @@ -338,14 +339,14 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad99d5b76aed5ed7bd752a9ef5c5e4acf74e8956df80080a492dc83e96d7067e" +checksum = "5630ce8552579d1393383b27fe4bfe7c700fb7480189a82fc054da24521947aa" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-primitives", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "serde", ] @@ -356,7 +357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4fda8b1920a38a5adc607d6ff7be1e8991e16ffcf97bb12765644b87331c598" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -378,9 +379,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c35fc4b03ace65001676358ffbbaefe2a2b27ee50fe777c345082c7c888be8" +checksum = "6a12fe11d0b8118e551c29e1a67ccb6d01cc07ef08086df30f07487146de6fa1" dependencies = [ "alloy-rlp", "arbitrary", @@ -409,13 +410,13 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39721fb4c0526ec2c98ad47f5010ea6930ba121e56f46a0ff2746ed761e92a40" +checksum = "959aedfc417737e2a59961c95e92c59726386748d85ef516a0d0687b440d3184" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-json-rpc", "alloy-network", "alloy-network-primitives", @@ -454,9 +455,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeb2dd970e7d70d2d408bc6fba391ad66406d766312399a42d6f93a9586f818" +checksum = "cf694cd1494284e73e23b62568bb5df6777f99eaec6c0a4705ac5a5c61707208" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -497,9 +498,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e59b7ec68078fcae32736052a5f56ffe1a01da267c10692757a9ae70698bb99" +checksum = "5f20436220214938c4fe223244f00fbd618dda80572b8ffe7839d382a6c54f1c" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -525,9 +526,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "933d9ad7e50156f1cd5574227e6f15c6d237d50006b1754e3f3564d6e8293ed5" +checksum = "88d2981f41486264b2e254bc51b2691bbef9ed87d0545d11f31cb26af3109cc4" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -535,38 +536,38 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-rpc-types-txpool", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "serde", ] [[package]] name = "alloy-rpc-types-anvil" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6895febd23a9fd75a58d2fb8fa27e6ed84f4aaada309c6905e156af37afbb6e6" +checksum = "3ebe3dcbc6c85678f29c205b2fcf6b110b32287bf6b72bbee37ed9011404e926" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "serde", ] [[package]] name = "alloy-rpc-types-any" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7233023bbb100b0527e5eb7ad9fe87b74a1dfed75bb202302c318f1cc68094ab" +checksum = "c583654aab419fe9e553ba86ab503e1cda0b855509ac95210c4ca6df84724255" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", ] [[package]] name = "alloy-rpc-types-debug" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e393e02f83d17384a7595d5288d1217cab0863ad5f10b0824325d814e7bf21c5" +checksum = "7913c67b874db23446a4cdd020da1bbc828513bd83536ccabfca403b71cdeaf9" dependencies = [ "alloy-primitives", "serde", @@ -574,15 +575,15 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8298431d08611686fb021210b12fb229657490e3bd0658acdd709a19886b6cdf" +checksum = "63b70151dc3282ce4bbde31b80a7f0f1e53b9dec9b187f528394e8f0a0411975" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-primitives", "alloy-rlp", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "derive_more 2.0.1", "jsonwebtoken", "rand 0.8.5", @@ -592,17 +593,17 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c2b3bd2a36df4417a349a5405b5130af948a72bf2f305fc0084cac5fbe5cc1" +checksum = "2c4496ab5f898c88e9153b88fcb6738e2d58b2ba6d7d85c3144ee83c990316a3" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-network-primitives", "alloy-primitives", "alloy-rlp", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-sol-types", "itertools 0.14.0", "serde", @@ -612,13 +613,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4c2404b7ee0bd81900d63c61adef877992f430d7c932d4f3ce2a79e3f3f822" +checksum = "bcf555fe777cf7d11b8ebe837aca0b0ceb74f1ed9937f938b8c9fbd1460994cf" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "serde", "serde_json", "thiserror 2.0.12", @@ -626,13 +627,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.0.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9f501c596cc75a973f0f9dd3dce796d18a9454426807f30c6d28f3141540f6" +checksum = "87c69ea401ce851b52c9b07d838173cd3c23c11a552a5e5ddde3ffd834647a46" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "serde", ] @@ -649,9 +650,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9f6973e552b33c9e0733165ef3f6cea3cc70617d5768260e98a29aab5e974e" +checksum = "a8c34ffc38f543bfdceed8c1fa5253fa5131455bb3654976be4cc3a4ae6d61f4" dependencies = [ "alloy-primitives", "serde", @@ -660,9 +661,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873c4e578c20af87b62b66d6f6c1cd81ab192f52515d4e63ddfecba1ac69216b" +checksum = "59245704a5dbd20b93913f4a20aa41b45c4c134f82e119bb54de4b627e003955" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -677,9 +678,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0b54cde20aba46a7330422b399ee5db6e45c16fcbf6073ba1cf34171ea2b95" +checksum = "5a24ea892abc29582dce6df5a1266ddf620fe93a04eb0265a3072c68c8b0ea10" dependencies = [ "alloy-consensus", "alloy-network", @@ -695,9 +696,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63ea5c8bbc889d3d5bfe42b965d5879fdeff317fdcaf3af5262a0f14e7b2a6a" +checksum = "f07be333cf6b3f06475d86b5fe39e5f94285714fffaf961173ff87448180f346" dependencies = [ "alloy-consensus", "alloy-network", @@ -713,9 +714,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19013fc470d4b2f454d34e20d5de3985c88ef5e0b3bb12fd7a8e344bccfba84e" +checksum = "04ad29d22519ce6fcf85edb8fb0d335216e8522c4458cdd92792f06c2173f9f2" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -733,9 +734,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bbc5479f66f49f06c91b69c4ffd33b4df3b6f286c55c2dc94fb7ac8571053a" +checksum = "eae78644ab0945e95efa2dc0cfac8f53aa1226fe85c294a0d8ad82c5cc9f09a2" dependencies = [ "alloy-consensus", "alloy-network", @@ -752,9 +753,9 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a00fdbc59ea2b77e497b29c358f658b23eedf60001184b7af24a57d77ed525" +checksum = "52a384f096c65ec6f9c541dd8a0d1217f38ff7aa0d2565254d03d9a51c23b5c6" dependencies = [ "alloy-consensus", "alloy-network", @@ -769,9 +770,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8612e0658964d616344f199ab251a49d48113992d81b92dab93ed855faa66383" +checksum = "5d3ef8e0d622453d969ba3cded54cf6800efdc85cb929fe22c5bdf8335666757" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -783,9 +784,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a384edac7283bc4c010a355fb648082860c04b826bb7a814c45263c8f304c74" +checksum = "f0e84bd0693c69a8fbe3ec0008465e029c6293494df7cb07580bf4a33eff52e1" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -802,9 +803,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd588c2d516da7deb421b8c166dc60b7ae31bca5beea29ab6621fcfa53d6ca5" +checksum = "f3de663412dadf9b64f4f92f507f78deebcc92339d12cf15f88ded65d41c7935" dependencies = [ "alloy-json-abi", "const-hex", @@ -820,9 +821,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86ddeb70792c7ceaad23e57d52250107ebbb86733e52f4a25d8dc1abc931837" +checksum = "251273c5aa1abb590852f795c938730fa641832fc8fa77b5478ed1bf11b6097e" dependencies = [ "serde", "winnow", @@ -830,21 +831,22 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584cb97bfc5746cb9dcc4def77da11694b5d6d7339be91b7480a6a68dc129387" +checksum = "5460a975434ae594fe2b91586253c1beb404353b78f0a55bf124abcd79557b15" dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-macro", + "const-hex", "serde", ] [[package]] name = "alloy-transport" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db88a480955a3a1a6dbcd51076b03f522c64fb4ecb74545c4d38b0ca58d9fbe6" +checksum = "a56afd0561a291e84de9d5616fa3def4c4925a09117ea3b08d4d5d207c4f7083" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -865,9 +867,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba0d8eace41128b4dbf87adff5d63347b8004977aa7f9c62b63308539dd2a29" +checksum = "90770711e649bb3df0a8a666ae7b80d1d77eff5462eaf6bb4d3eaf134f6e636e" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -880,9 +882,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8d9fcdeeaeb9bfa08bc70031c9518f0d9d31766c0fa71500737b4a4a215249" +checksum = "983693379572a06e2bc1050116d975395604b357e1f2ac4420dd385d9ee18c11" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -900,9 +902,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d39c87d5b9f3dd8e8efdc95bd3aaeef947e7938dbf45cffda9bd5689cd9517" +checksum = "90569cc2e13f5cdf53f42d5b3347cf4a89fccbcf9978cf08b165b4a1c6447672" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1034,12 +1036,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "once_cell_polyfill", + "once_cell", "windows-sys 0.59.0", ] @@ -1051,7 +1053,7 @@ dependencies = [ "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-evm", "alloy-genesis", "alloy-network", @@ -1061,7 +1063,7 @@ dependencies = [ "alloy-pubsub", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -1113,12 +1115,12 @@ version = "1.2.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "bytes", "foundry-common", "foundry-evm", @@ -1704,9 +1706,9 @@ dependencies = [ [[package]] name = "aws-sdk-kms" -version = "1.71.0" +version = "1.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de993b5250e1aa051f4091ab772ce164de8c078ee9793fdee033b20f7d371ad" +checksum = "c64c93b24f98760979113386e444886fc812632d4d84910b802d69a2bdbb5349" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1726,9 +1728,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.70.0" +version = "1.68.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83447efb7179d8e2ad2afb15ceb9c113debbc2ecdf109150e338e2e28b86190b" +checksum = "bd5f01ea61fed99b5fe4877abff6c56943342a56ff145e9e0c7e2494419008be" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1748,9 +1750,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.71.0" +version = "1.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f9bfbbda5e2b9fe330de098f14558ee8b38346408efe9f2e9cee82dc1636a4" +checksum = "27454e4c55aaa4ef65647e3a1cf095cb834ca6d54e959e2909f1fef96ad87860" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1770,9 +1772,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.71.0" +version = "1.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17b984a66491ec08b4f4097af8911251db79296b3e4a763060b45805746264f" +checksum = "ffd6ef5d00c94215960fabcdf2d9fe7c090eed8be482d66d47b92d4aba1dd4aa" dependencies = [ "aws-credential-types", "aws-runtime", @@ -2389,7 +2391,7 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -2581,9 +2583,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "3.0.3" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" dependencies = [ "clap", "log", @@ -3814,7 +3816,7 @@ dependencies = [ "alloy-primitives", "alloy-provider", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-signer", "alloy-signer-local", "alloy-transport", @@ -3947,13 +3949,13 @@ dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-json-abi", "alloy-network", "alloy-primitives", "alloy-provider", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-signer", "clap", "dialoguer", @@ -4145,7 +4147,7 @@ version = "1.2.1" dependencies = [ "alloy-chains", "alloy-dyn-abi", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-ens", "alloy-json-abi", "alloy-primitives", @@ -4191,7 +4193,7 @@ version = "1.2.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-json-abi", "alloy-json-rpc", "alloy-network", @@ -4200,7 +4202,7 @@ dependencies = [ "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "alloy-sol-types", "alloy-transport", "alloy-transport-http", @@ -4249,7 +4251,7 @@ dependencies = [ "alloy-network", "alloy-primitives", "alloy-rpc-types", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "chrono", "foundry-macros", "revm", @@ -4261,9 +4263,9 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.16.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a43ecf9100de8d31242b7a7cf965196e3d8fb424a931a975a7882897726f4c46" +checksum = "3d6154e503612a175a88ff342592f0a44664dda54a508d9443121b62b1701f91" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4298,9 +4300,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" -version = "0.16.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07ebb99f087075b97b68837de385f6cba07ca32314852a6507ce0dac5d66cd8" +checksum = "efca59ffe52914a8ff4ae4e8e97e9fa5d782031d2f5a2d1fb109fa6aac26653d" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -4308,9 +4310,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" -version = "0.16.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ddf6d61defb08103e351dc16df637c42d69407bae0f8789f2662a6161e73f5" +checksum = "0a3ffc93f94d2c4ae0ff0f7a12cdeaa5fbb043ced0c558cabd05631e32ac508a" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4331,9 +4333,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" -version = "0.16.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65a777326b8e0bba45345043ed80ceab5e84f3e474ba2ac19673b31076f3c6b4" +checksum = "5f077777aa33f933f9f01e90f9ed78e4a40ed60a6380798b528681ca9519a577" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4346,9 +4348,9 @@ dependencies = [ [[package]] name = "foundry-compilers-core" -version = "0.16.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be02912b222eb5bd8a4e8a249a488af3e2fb4fc08e0f17a3bc402406a91295b5" +checksum = "8ff92d5831075077fe64391dcbbe4a1518bce0439f97ab6a940fa1b9c1f3eaa2" dependencies = [ "alloy-primitives", "cfg-if", @@ -5228,9 +5230,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.12" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -5543,9 +5545,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -5558,9 +5560,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" dependencies = [ "proc-macro2", "quote", @@ -5684,9 +5686,9 @@ dependencies = [ [[package]] name = "lalrpop" -version = "0.22.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +checksum = "7047a26de42016abf8f181b46b398aef0b77ad46711df41847f6ed869a2a1d5b" dependencies = [ "ascii-canvas", "bit-set", @@ -5705,9 +5707,9 @@ dependencies = [ [[package]] name = "lalrpop-util" -version = "0.22.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +checksum = "e8d05b3fe34b8bd562c338db725dfa9beb9451a48f65f129ccb9538b48d2c93b" dependencies = [ "regex-automata 0.4.9", "rustversion", @@ -6469,12 +6471,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - [[package]] name = "once_map" version = "0.4.21" @@ -6494,12 +6490,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f318b09e24148f07392c5e011bae047a0043851f9041145df5f3b01e4fedd1e" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "derive_more 2.0.1", "serde", "thiserror 2.0.12", @@ -6518,11 +6514,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15ede8322c10c21249de4fced204e2af4978972e715afee34b6fe684d73880cf" dependencies = [ "alloy-consensus", - "alloy-eips 1.0.6", + "alloy-eips 1.0.5", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 1.0.6", + "alloy-serde 1.0.5", "derive_more 2.0.1", "op-alloy-consensus", "serde", @@ -6618,9 +6614,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.7.5" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" dependencies = [ "arrayvec", "bitvec", @@ -6634,9 +6630,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.5" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -7840,9 +7836,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.15.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11256b5fe8c68f56ac6f39ef0720e592f33d2367a4782740d9c9142e889c7fb4" +checksum = "78a46eb779843b2c4f21fac5773e25d6d5b7c8f0922876c91541790d2ca27eef" dependencies = [ "alloy-rlp", "arbitrary", @@ -8026,9 +8022,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -9059,9 +9055,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.1.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d879005cc1b5ba4e18665be9e9501d9da3a9b95f625497c4cb7ee082b532e" +checksum = "3d0f0d4760f4c2a0823063b2c70e97aa2ad185f57be195172ccc0e23c4b787c4" dependencies = [ "paste", "proc-macro2", @@ -10357,15 +10353,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", - "windows-strings 0.4.2", + "windows-strings 0.4.1", ] [[package]] @@ -10430,9 +10426,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" dependencies = [ "windows-link", ] @@ -10448,9 +10444,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" dependencies = [ "windows-link", ] From 53893fd374c9084ea189ea590102b22b7508ee78 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 23 May 2025 08:11:28 +0300 Subject: [PATCH 106/107] Preserve alloy patch placeholders --- Cargo.toml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bb2d53863e670..76f51d5337151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -364,6 +364,31 @@ zip-extract = "=0.2.1" # alloy-sol-types = { path = "../../alloy-rs/core/crates/sol-types" } # syn-solidity = { path = "../../alloy-rs/core/crates/syn-solidity" } +## alloy +# alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-serde = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-aws = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-gcp = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-ledger = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-trezor = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } + ## alloy-evm # alloy-evm = { git = "https://github.com/alloy-rs/evm.git", rev = "95f6a8a" } # alloy-op-evm = { git = "https://github.com/alloy-rs/evm.git", rev = "95f6a8a" } @@ -373,6 +398,5 @@ revm = { git = "https://github.com/bluealloy/revm.git", rev = "b5808253" } op-revm = { git = "https://github.com/bluealloy/revm.git", rev = "b5808253" } # revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors.git", rev = "a625c04" } - ## foundry # foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-fork-db", rev = "811a61a" } From 75eaf09f5ace1d3225c861f70a4356e4387d2ee0 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 23 May 2025 08:17:31 +0300 Subject: [PATCH 107/107] Clippy --- crates/anvil/core/src/eth/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 02d18e209c34e..a4d05ed29562f 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -35,7 +35,6 @@ pub struct Params { /// Represents ethereum JSON-RPC API #[derive(Clone, Debug, serde::Deserialize)] #[serde(tag = "method", content = "params")] -#[expect(clippy::large_enum_variant)] pub enum EthRequest { #[serde(rename = "web3_clientVersion", with = "empty_params")] Web3ClientVersion(()),