diff --git a/crates/config/src/lint.rs b/crates/config/src/lint.rs index 6752372767a2b..7a87a47321d21 100644 --- a/crates/config/src/lint.rs +++ b/crates/config/src/lint.rs @@ -26,11 +26,8 @@ pub struct LinterConfig { /// Defaults to true. Set to false to disable automatic linting during builds. pub lint_on_build: bool, - /// Configurable patterns that should be excluded when performing `mixedCase` lint checks. - /// - /// Default's to ["ERC", "URI"] to allow common names like `rescueERC20`, `ERC721TokenReceiver` - /// or `tokenURI`. - pub mixed_case_exceptions: Vec, + /// Configuration specific to individual lints. + pub lint_specific: LintSpecificConfig, } impl Default for LinterConfig { @@ -40,7 +37,44 @@ impl Default for LinterConfig { severity: Vec::new(), exclude_lints: Vec::new(), ignore: Vec::new(), + lint_specific: LintSpecificConfig::default(), + } + } +} + +/// Contract types that can be exempted from the multi-contract-file lint. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ContractException { + Interface, + Library, + AbstractContract, +} + +/// Configuration specific to individual lints. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct LintSpecificConfig { + /// Configurable patterns that should be excluded when performing `mixedCase` lint checks. + /// + /// Defaults to ["ERC", "URI"] to allow common names like `rescueERC20`, `ERC721TokenReceiver` + /// or `tokenURI`. + pub mixed_case_exceptions: Vec, + + /// Contract types that are allowed to appear multiple times in the same file. + /// + /// Valid values: "interface", "library", "abstract_contract" + /// + /// Defaults to an empty array (all contract types are flagged when multiple exist). + /// Note: Regular contracts cannot be exempted and will always be flagged when multiple exist. + pub multi_contract_file_exceptions: Vec, +} + +impl Default for LintSpecificConfig { + fn default() -> Self { + Self { mixed_case_exceptions: vec!["ERC".to_string(), "URI".to_string()], + multi_contract_file_exceptions: Vec::new(), } } } diff --git a/crates/forge/src/cmd/build.rs b/crates/forge/src/cmd/build.rs index d37c73fd68cbb..5ae1247de4b5b 100644 --- a/crates/forge/src/cmd/build.rs +++ b/crates/forge/src/cmd/build.rs @@ -151,7 +151,7 @@ impl BuildArgs { .collect(), ) }) - .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions); + .with_lint_specific(&config.lint.lint_specific); // Expand ignore globs and canonicalize from the get go let ignored = expand_globs(&config.root, config.lint.ignore.iter())? diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index ae4b8389a8ecc..a4663b2bce766 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -104,7 +104,7 @@ impl LintArgs { .with_lints(include) .without_lints(exclude) .with_severity(if severity.is_empty() { None } else { Some(severity) }) - .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions); + .with_lint_specific(&config.lint.lint_specific); let output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?; let solar_sources = get_solar_sources_from_compile_output(&config, &output, Some(&input))?; diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 6a32ae043b19f..688c5b5fa5ef2 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -146,10 +146,13 @@ severity = [] exclude_lints = [] ignore = [] lint_on_build = true + +[lint.lint_specific] mixed_case_exceptions = [ "ERC", "URI", ] +multi_contract_file_exceptions = [] [doc] out = "docs" @@ -1339,10 +1342,13 @@ forgetest_init!(test_default_config, |prj, cmd| { "exclude_lints": [], "ignore": [], "lint_on_build": true, - "mixed_case_exceptions": [ - "ERC", - "URI" - ] + "lint_specific": { + "mixed_case_exceptions": [ + "ERC", + "URI" + ], + "multi_contract_file_exceptions": [] + } }, "doc": { "out": "docs", diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index da766cf099173..0d6e9d0b48e11 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -1,5 +1,5 @@ use forge_lint::{linter::Lint, sol::med::REGISTERED_LINTS}; -use foundry_config::{DenyLevel, LintSeverity, LinterConfig}; +use foundry_config::{DenyLevel, LintSeverity, LinterConfig, lint::LintSpecificConfig}; mod geiger; @@ -112,6 +112,57 @@ contract CounterTest { } "#; +const MULTI_CONTRACT_FILE: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IToken { + function transfer(address to, uint256 amount) external returns (bool); +} + +library MathLib { + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } +} + +contract FirstContract { + uint256 public value; + + function setValue(uint256 _value) public { + value = _value; + } +} + +abstract contract BaseContract { + function baseFunction() public virtual; +} + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); +} + +contract SecondContract { + address public owner; + + constructor() { + owner = msg.sender; + } +} + +library StringLib { + function toUpperCase(string memory str) internal pure returns (string memory) { + return str; + } +} + +abstract contract AbstractStorage { + mapping(address => uint256) internal balances; + + function getBalance(address account) public view virtual returns (uint256); +} +"#; + forgetest!(can_use_config, |prj, cmd| { prj.add_source("ContractWithLints", CONTRACT); prj.add_source("OtherContractWithLints", OTHER_CONTRACT); @@ -189,12 +240,246 @@ forgetest!(can_use_config_mixed_case_exception, |prj, cmd| { exclude_lints: vec![], ignore: vec!["src/ContractWithLints.sol".into()], lint_on_build: true, - mixed_case_exceptions: vec!["MIXED".to_string()], + lint_specific: LintSpecificConfig { + mixed_case_exceptions: vec!["MIXED".to_string()], + ..Default::default() + }, }; }); cmd.arg("lint").assert_success().stderr_eq(str![[""]]); }); +forgetest!(multi_contract_file_no_exceptions, |prj, cmd| { + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // Without exceptions, should flag all 8 contract-like items + prj.update_config(|config| { + config.lint = LinterConfig { lint_on_build: true, ..Default::default() }; + }); + + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Should see 8 instances of multi-contract-file lint + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 8); + assert!(stderr.contains("IToken")); + assert!(stderr.contains("IERC20")); + assert!(stderr.contains("MathLib")); + assert!(stderr.contains("StringLib")); + assert!(stderr.contains("BaseContract")); + assert!(stderr.contains("AbstractStorage")); + assert!(stderr.contains("FirstContract")); + assert!(stderr.contains("SecondContract")); +}); + +forgetest!(multi_contract_file_interface_exception, |prj, cmd| { + use foundry_config::lint::ContractException; + + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // With interface exception, should flag 6 items + prj.update_config(|config| { + config.lint = LinterConfig { + lint_on_build: true, + lint_specific: LintSpecificConfig { + multi_contract_file_exceptions: vec![ContractException::Interface], + ..Default::default() + }, + ..Default::default() + }; + }); + + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Should see 6 instances (2 interfaces excluded) + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 6); + assert!(!stderr.contains("IToken")); + assert!(!stderr.contains("IERC20")); + assert!(stderr.contains("MathLib")); + assert!(stderr.contains("FirstContract")); +}); + +forgetest!(multi_contract_file_library_exception, |prj, cmd| { + use foundry_config::lint::ContractException; + + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // With library exception, should flag 6 items + prj.update_config(|config| { + config.lint = LinterConfig { + lint_on_build: true, + lint_specific: LintSpecificConfig { + multi_contract_file_exceptions: vec![ContractException::Library], + ..Default::default() + }, + ..Default::default() + }; + }); + + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Should see 6 instances (2 libraries excluded) + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 6); + assert!(stderr.contains("IToken")); + assert!(!stderr.contains("MathLib")); + assert!(!stderr.contains("StringLib")); + assert!(stderr.contains("FirstContract")); +}); + +forgetest!(multi_contract_file_abstract_exception, |prj, cmd| { + use foundry_config::lint::ContractException; + + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // With abstract contract exception, should flag 6 items + prj.update_config(|config| { + config.lint = LinterConfig { + lint_on_build: true, + lint_specific: LintSpecificConfig { + multi_contract_file_exceptions: vec![ContractException::AbstractContract], + ..Default::default() + }, + ..Default::default() + }; + }); + + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Should see 6 instances (2 abstract contracts excluded) + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 6); + assert!(stderr.contains("IToken")); + assert!(stderr.contains("MathLib")); + assert!(!stderr.contains("BaseContract")); + assert!(!stderr.contains("AbstractStorage")); + assert!(stderr.contains("FirstContract")); +}); + +forgetest!(multi_contract_file_multiple_exceptions, |prj, cmd| { + use foundry_config::lint::ContractException; + + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // With interface + library exceptions, should flag 4 items + prj.update_config(|config| { + config.lint = LinterConfig { + lint_on_build: true, + lint_specific: LintSpecificConfig { + multi_contract_file_exceptions: vec![ + ContractException::Interface, + ContractException::Library, + ], + ..Default::default() + }, + ..Default::default() + }; + }); + + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Should see 4 instances (2 interfaces + 2 libraries excluded) + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 4); + assert!(!stderr.contains("IToken")); + assert!(!stderr.contains("MathLib")); + assert!(stderr.contains("BaseContract")); + assert!(stderr.contains("FirstContract")); +}); + +forgetest!(multi_contract_file_all_exceptions, |prj, cmd| { + use foundry_config::lint::ContractException; + + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // With all exceptions, should still flag 2 regular contracts + prj.update_config(|config| { + config.lint = LinterConfig { + lint_on_build: true, + lint_specific: LintSpecificConfig { + multi_contract_file_exceptions: vec![ + ContractException::Interface, + ContractException::Library, + ContractException::AbstractContract, + ], + ..Default::default() + }, + ..Default::default() + }; + }); + + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Should see 2 instances (only the 2 regular contracts) + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 2); + assert!(!stderr.contains("IToken")); + assert!(!stderr.contains("MathLib")); + assert!(!stderr.contains("BaseContract")); + assert!(stderr.contains("FirstContract")); + assert!(stderr.contains("SecondContract")); +}); + +forgetest!(multi_contract_file_invalid_toml_value, |prj, cmd| { + use std::fs; + + prj.add_source("Simple", "contract Simple {}"); + + // Write invalid TOML config with invalid enum value + let config_path = prj.root().join("foundry.toml"); + let invalid_config = r#" +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +[profile.default.lint.lint_specific] +multi_contract_file_exceptions = ["interface", "bad_contract_type", "library"] +"#; + + fs::write(&config_path, invalid_config).unwrap(); + + // Should fail with deserialization error + let output = cmd.arg("lint").assert_failure(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + // Assert specific error message for invalid enum variant + assert!(stderr.contains("unknown variant")); + assert!(stderr.contains("expected `one of `interface`, `library`, `abstract_contract`")); +}); + +forgetest!(multi_contract_file_valid_toml_values, |prj, cmd| { + use std::fs; + + prj.add_source("MixedFile", MULTI_CONTRACT_FILE); + + // Write valid TOML config with all valid enum values + let config_path = prj.root().join("foundry.toml"); + let valid_config = r#" +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +[profile.default.lint] +lint_on_build = true + +[profile.default.lint.lint_specific] +multi_contract_file_exceptions = ["interface", "library", "abstract_contract"] +"#; + + fs::write(&config_path, valid_config).unwrap(); + + // Should succeed and only flag the 2 regular contracts + let output = cmd.arg("lint").assert_success(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + + assert_eq!(stderr.matches("note[multi-contract-file]").count(), 2); + assert!(stderr.contains("FirstContract")); + assert!(stderr.contains("SecondContract")); +}); + forgetest!(can_override_config_severity, |prj, cmd| { prj.add_source("ContractWithLints", CONTRACT); prj.add_source("OtherContractWithLints", OTHER_CONTRACT); diff --git a/crates/lint/src/linter/mod.rs b/crates/lint/src/linter/mod.rs index 3ac445744a10a..11e1ba17e5705 100644 --- a/crates/lint/src/linter/mod.rs +++ b/crates/lint/src/linter/mod.rs @@ -6,7 +6,10 @@ pub use late::{LateLintPass, LateLintVisitor}; use foundry_common::comments::inline_config::InlineConfig; use foundry_compilers::Language; -use foundry_config::{DenyLevel, lint::Severity}; +use foundry_config::{ + DenyLevel, + lint::{LintSpecificConfig, Severity}, +}; use solar::{ interface::{ Session, Span, @@ -55,7 +58,7 @@ pub struct LintContext<'s, 'c> { pub struct LinterConfig<'s> { pub inline: &'s InlineConfig>, - pub mixed_case_exceptions: &'s [String], + pub lint_specific: &'s LintSpecificConfig, } impl<'s, 'c> LintContext<'s, 'c> { diff --git a/crates/lint/src/sol/info/mixed_case.rs b/crates/lint/src/sol/info/mixed_case.rs index c128d40b55ec8..669772188c2a3 100644 --- a/crates/lint/src/sol/info/mixed_case.rs +++ b/crates/lint/src/sol/info/mixed_case.rs @@ -15,8 +15,11 @@ declare_forge_lint!( 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 Some(expected) = - check_mixed_case(name.as_str(), true, ctx.config.mixed_case_exceptions) + && let Some(expected) = check_mixed_case( + name.as_str(), + true, + &ctx.config.lint_specific.mixed_case_exceptions, + ) && !is_constant_getter(&func.header) { ctx.emit_with_suggestion( @@ -47,8 +50,11 @@ impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable { ) { if var.mutability.is_none() && let Some(name) = var.name - && let Some(expected) = - check_mixed_case(name.as_str(), false, ctx.config.mixed_case_exceptions) + && let Some(expected) = check_mixed_case( + name.as_str(), + false, + &ctx.config.lint_specific.mixed_case_exceptions, + ) { ctx.emit_with_suggestion( &MIXED_CASE_VARIABLE, diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index ddb18c6e30960..b87bf900193ca 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -18,6 +18,9 @@ use named_struct_fields::NAMED_STRUCT_FIELDS; mod unsafe_cheatcodes; use unsafe_cheatcodes::UNSAFE_CHEATCODE_USAGE; +mod multi_contract_file; +use multi_contract_file::MULTI_CONTRACT_FILE; + register_lints!( (PascalCaseStruct, early, (PASCAL_CASE_STRUCT)), (MixedCaseVariable, early, (MIXED_CASE_VARIABLE)), @@ -25,5 +28,6 @@ register_lints!( (ScreamingSnakeCase, early, (SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE)), (Imports, early, (UNALIASED_PLAIN_IMPORT, UNUSED_IMPORT)), (NamedStructFields, late, (NAMED_STRUCT_FIELDS)), - (UnsafeCheatcodes, early, (UNSAFE_CHEATCODE_USAGE)) + (UnsafeCheatcodes, early, (UNSAFE_CHEATCODE_USAGE)), + (MultiContractFile, early, (MULTI_CONTRACT_FILE)) ); diff --git a/crates/lint/src/sol/info/multi_contract_file.rs b/crates/lint/src/sol/info/multi_contract_file.rs new file mode 100644 index 0000000000000..2e56244850f1f --- /dev/null +++ b/crates/lint/src/sol/info/multi_contract_file.rs @@ -0,0 +1,56 @@ +use crate::{ + linter::{EarlyLintPass, Lint, LintContext}, + sol::{Severity, SolLint, info::MultiContractFile}, +}; + +use foundry_config::lint::ContractException; +use solar::ast::{self as ast}; + +declare_forge_lint!( + MULTI_CONTRACT_FILE, + Severity::Info, + "multi-contract-file", + "prefer having only one contract, interface or library per file" +); + +impl<'ast> EarlyLintPass<'ast> for MultiContractFile { + fn check_full_source_unit( + &mut self, + ctx: &LintContext<'ast, '_>, + unit: &'ast ast::SourceUnit<'ast>, + ) { + if !ctx.is_lint_enabled(MULTI_CONTRACT_FILE.id()) { + return; + } + + // Check which types are exempted + let exceptions = &ctx.config.lint_specific.multi_contract_file_exceptions; + let should_lint_interfaces = !exceptions.contains(&ContractException::Interface); + let should_lint_libraries = !exceptions.contains(&ContractException::Library); + let should_lint_abstract = !exceptions.contains(&ContractException::AbstractContract); + + // Collect spans of all contract-like items, skipping those that are exempted + let relevant_spans: Vec<_> = unit + .items + .iter() + .filter_map(|item| match &item.kind { + ast::ItemKind::Contract(c) => { + let should_lint = match c.kind { + ast::ContractKind::Interface => should_lint_interfaces, + + ast::ContractKind::Library => should_lint_libraries, + ast::ContractKind::AbstractContract => should_lint_abstract, + ast::ContractKind::Contract => true, // Regular contracts are always linted + }; + should_lint.then_some(c.name.span) + } + _ => None, + }) + .collect(); + + // Flag all if there's more than one + if relevant_spans.len() > 1 { + relevant_spans.into_iter().for_each(|span| ctx.emit(&MULTI_CONTRACT_FILE, span)); + } + } +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index ed72b0724a3a4..d6d426a057865 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -11,7 +11,10 @@ use foundry_common::{ sh_warn, }; use foundry_compilers::{ProjectPathsConfig, solc::SolcLanguage}; -use foundry_config::{DenyLevel, lint::Severity}; +use foundry_config::{ + DenyLevel, + lint::{LintSpecificConfig, Severity}, +}; use rayon::prelude::*; use solar::{ ast::{self as ast, visit::Visit as _}, @@ -49,6 +52,9 @@ static ALL_REGISTERED_LINTS: LazyLock> = LazyLock::new(|| { lints.into_iter().map(|lint| lint.id()).collect() }); +static DEFAULT_LINT_SPECIFIC_CONFIG: LazyLock = + LazyLock::new(LintSpecificConfig::default); + /// Linter implementation to analyze Solidity source code responsible for identifying /// vulnerabilities gas optimizations, and best practices. #[derive(Debug)] @@ -60,7 +66,7 @@ pub struct SolidityLinter<'a> { with_description: bool, with_json_emitter: bool, // lint-specific configuration - mixed_case_exceptions: &'a [String], + lint_specific: &'a LintSpecificConfig, } impl<'a> SolidityLinter<'a> { @@ -72,7 +78,7 @@ impl<'a> SolidityLinter<'a> { lints_included: None, lints_excluded: None, with_json_emitter: false, - mixed_case_exceptions: &[], + lint_specific: &DEFAULT_LINT_SPECIFIC_CONFIG, } } @@ -101,13 +107,13 @@ impl<'a> SolidityLinter<'a> { self } - pub fn with_mixed_case_exceptions(mut self, exceptions: &'a [String]) -> Self { - self.mixed_case_exceptions = exceptions; + pub fn with_lint_specific(mut self, lint_specific: &'a LintSpecificConfig) -> Self { + self.lint_specific = lint_specific; self } fn config(&'a self, inline: &'a InlineConfig>) -> LinterConfig<'a> { - LinterConfig { inline, mixed_case_exceptions: self.mixed_case_exceptions } + LinterConfig { inline, lint_specific: self.lint_specific } } fn include_lint(&self, lint: SolLint) -> bool { diff --git a/crates/lint/testdata/Keccak256.stderr b/crates/lint/testdata/Keccak256.stderr index b980f45732189..199d5217b34ea 100644 --- a/crates/lint/testdata/Keccak256.stderr +++ b/crates/lint/testdata/Keccak256.stderr @@ -38,6 +38,30 @@ LL | uint256 Enabled_MixedCase_Variable = 1; | = help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/Keccak256.sol:LL:CC + | +LL | contract AsmKeccak256 { + | ^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/Keccak256.sol:LL:CC + | +LL | contract OtherAsmKeccak256 { + | ^^^^^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/Keccak256.sol:LL:CC + | +LL | contract YetAnotherAsmKeccak256 { + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly --> ROOT/testdata/Keccak256.sol:LL:CC | diff --git a/crates/lint/testdata/MixedCase.stderr b/crates/lint/testdata/MixedCase.stderr index 654b7e2f171c5..05a759c65f381 100644 --- a/crates/lint/testdata/MixedCase.stderr +++ b/crates/lint/testdata/MixedCase.stderr @@ -134,3 +134,19 @@ LL | function NOT_ELEMENTARY_RETURN() external view returns (uint256[] memor | = help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MixedCase.sol:LL:CC + | +LL | interface IERC20 { + | ^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MixedCase.sol:LL:CC + | +LL | contract MixedCaseTest { + | ^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + diff --git a/crates/lint/testdata/MultiContractFile.sol b/crates/lint/testdata/MultiContractFile.sol new file mode 100644 index 0000000000000..02ab04124f3d0 --- /dev/null +++ b/crates/lint/testdata/MultiContractFile.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract A {} + +contract B {} //~NOTE: prefer having only one contract, interface or library per file + +contract C {} + +interface I {} + +library L {} diff --git a/crates/lint/testdata/MultiContractFile.stderr b/crates/lint/testdata/MultiContractFile.stderr new file mode 100644 index 0000000000000..38d2300a080b8 --- /dev/null +++ b/crates/lint/testdata/MultiContractFile.stderr @@ -0,0 +1,40 @@ +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile.sol:LL:CC + | +LL | contract A {} + | ^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile.sol:LL:CC + | +LL | contract B {} + | ^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile.sol:LL:CC + | +LL | contract C {} + | ^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile.sol:LL:CC + | +LL | interface I {} + | ^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile.sol:LL:CC + | +LL | library L {} + | ^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + diff --git a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.sol b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.sol new file mode 100644 index 0000000000000..ca9e6c21c266b --- /dev/null +++ b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +// Interface counts as a contract-like item. +interface I1 {} + +// Library is also a contract-like item and it should be counted. +library L1 {} //~NOTE: prefer having only one contract, interface or library per file + +// Third contract-like item. +contract C1 {} + diff --git a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr new file mode 100644 index 0000000000000..91e0fcc7960c9 --- /dev/null +++ b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr @@ -0,0 +1,24 @@ +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC + | +LL | interface I1 {} + | ^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC + | +LL | library L1 {} + | ^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC + | +LL | contract C1 {} + | ^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + diff --git a/crates/lint/testdata/UncheckedTransferERC20.stderr b/crates/lint/testdata/UncheckedTransferERC20.stderr index 1afa4d689a871..b35774c796198 100644 --- a/crates/lint/testdata/UncheckedTransferERC20.stderr +++ b/crates/lint/testdata/UncheckedTransferERC20.stderr @@ -1,3 +1,43 @@ +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UncheckedTransferERC20.sol:LL:CC + | +LL | interface IERC20 { + | ^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UncheckedTransferERC20.sol:LL:CC + | +LL | interface IERC20Wrapper { + | ^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UncheckedTransferERC20.sol:LL:CC + | +LL | contract UncheckedTransfer { + | ^^^^^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UncheckedTransferERC20.sol:LL:CC + | +LL | library Currency { + | ^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UncheckedTransferERC20.sol:LL:CC + | +LL | contract UncheckedTransferUsingCurrencyLib { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value --> ROOT/testdata/UncheckedTransferERC20.sol:LL:CC | diff --git a/crates/lint/testdata/UnsafeTypecast.stderr b/crates/lint/testdata/UnsafeTypecast.stderr index 4a3f35b2275a4..ddc451733e506 100644 --- a/crates/lint/testdata/UnsafeTypecast.stderr +++ b/crates/lint/testdata/UnsafeTypecast.stderr @@ -1,3 +1,19 @@ +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UnsafeTypecast.sol:LL:CC + | +LL | contract UnsafeTypecast { + | ^^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UnsafeTypecast.sol:LL:CC + | +LL | contract Repros { + | ^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + warning[unsafe-typecast]: typecasts that can truncate values should be checked --> ROOT/testdata/UnsafeTypecast.sol:LL:CC | diff --git a/crates/lint/testdata/UnwrappedModifierLogic.stderr b/crates/lint/testdata/UnwrappedModifierLogic.stderr index d5d02817d0c04..65a733e8cab6a 100644 --- a/crates/lint/testdata/UnwrappedModifierLogic.stderr +++ b/crates/lint/testdata/UnwrappedModifierLogic.stderr @@ -1,3 +1,27 @@ +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC + | +LL | library Lib { + | ^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC + | +LL | contract C { + | ^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + --> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC + | +LL | contract UnwrappedModifierLogicTest { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size --> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC |