diff --git a/.changelog/lint-test-script-exclusions.md b/.changelog/lint-test-script-exclusions.md new file mode 100644 index 0000000000000..915b01ee80720 --- /dev/null +++ b/.changelog/lint-test-script-exclusions.md @@ -0,0 +1,6 @@ +--- +forge: patch +forge-lint: patch +--- + +Skip lints in configured test and script directories, including noisy state initializer checks, while preserving selected unsafe-cheatcode diagnostics. diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index 7cd66e6f8c15d..a07817caa8825 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -427,6 +427,162 @@ note[unused-import]: unused imports should be removed ╰ help: https://getfoundry.sh/forge/linting/unused-import +"#]]); +}); + +forgetest!(skip_test_and_script_lints, |prj, cmd| { + let fixture = r#" +contract Initializer { + address bob = makeAddr("bob"); + + function makeAddr(string memory name) internal returns (address) { + return address(uint160(uint256(keccak256(bytes(name))))); + } + + function incorrectShift(uint256 value) public pure { + assembly { pop(shr(value, 1)) } + } +} +"#; + prj.add_test("Initializer", fixture); + prj.add_script("Initializer", fixture); + + cmd.args(["build"]).assert_success().stderr_eq(""); + cmd.forge_fuse().arg("lint").assert_success().stderr_eq(""); + cmd.forge_fuse() + .args(["lint", "--severity", "high", "med", "low", "info", "gas", "code-size"]) + .assert_success() + .stderr_eq(""); + cmd.forge_fuse() + .args(["lint", "--only-lint", "function-init-state", "incorrect-shift"]) + .assert_success() + .stderr_eq(""); + + std::fs::rename(prj.root().join("test"), prj.root().join("checks")).unwrap(); + std::fs::rename(prj.root().join("script"), prj.root().join("deploy")).unwrap(); + prj.update_config(|config| { + config.test = "checks".into(); + config.script = "deploy".into(); + }); + cmd.forge_fuse() + .args(["lint", "--only-lint", "function-init-state", "incorrect-shift"]) + .assert_success() + .stderr_eq(""); + + prj.add_source("Initializer", fixture); + cmd.forge_fuse() + .args(["lint", "--only-lint", "function-init-state"]) + .assert_success() + .stderr_eq(str![[r#" +note[function-init-state]: state variable initializer depends on a non-pure function or another state variable + [FILE]:5:5 + │ +5 │ address bob = makeAddr("bob"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/function-init-state + + +"#]]); +}); + +forgetest!(unsafe_cheatcode_lints_tests_and_scripts, |prj, cmd| { + let fixture = r#" +interface Vm { + function readFile(string calldata path) external returns (string memory); +} + +contract Unsafe { + function read(Vm vm) external { + vm.readFile("input.txt"); + } +} +"#; + let test = prj.add_test("Unsafe", fixture); + let script = prj.add_script("Unsafe", fixture); + + cmd.arg("lint").assert_success().stderr_eq(""); + for path in [test, script] { + cmd.forge_fuse() + .arg("lint") + .arg(path) + .args(["--only-lint", "unsafe-cheatcode"]) + .assert_success() + .stderr_eq(str![[r#" +note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations + [FILE]:10:12 + │ +10 │ vm.readFile("input.txt"); + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode + + +"#]]); + } + + prj.update_config(|config| { + config.lint.severity = vec![LintSeverity::Info]; + config.lint.exclude_lints = vec!["unsafe-cheatcode".into()]; + }); + cmd.forge_fuse().arg("lint").assert_success().stderr_eq(""); +}); + +forgetest!(skip_test_and_script_project_lints, |prj, cmd| { + prj.add_test("Test", "pragma solidity ^0.8.0; contract Test {}"); + prj.add_script("Script", "pragma solidity >=0.8.0; contract Script {}"); + + cmd.args(["lint", "--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(""); +}); + +// +forgetest!(skip_reentrancy_events_for_expect_emit, |prj, cmd| { + let fixture = r#" +interface Vm { + function expectEmit(bool, bool, bool, bool, address) external; + function prank(address) external; +} + +interface Governance { + event Rejected(uint256 taskId, address owner); + function removeOwner(address owner) external; + function vetoRemoveOwner(address owner) external; +} + +contract EventExpectation { + function checkEvent(Vm vm, Governance harness, address alice, address bob) external { + vm.prank(bob); + harness.removeOwner(bob); + vm.expectEmit(true, true, false, false, address(harness)); + emit Governance.Rejected(1, alice); + vm.prank(alice); + harness.vetoRemoveOwner(bob); + } +} +"#; + prj.add_test("EventExpectation", fixture); + prj.add_script("EventExpectation", fixture); + + cmd.arg("lint").assert_success().stderr_eq(""); + cmd.forge_fuse() + .args(["lint", "--only-lint", "reentrancy-events"]) + .assert_success() + .stderr_eq(""); + + prj.add_source("EventExpectation", fixture); + cmd.forge_fuse() + .args(["lint", "--only-lint", "reentrancy-events"]) + .assert_success() + .stderr_eq(str![[r#" +warning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on + [FILE]:20:9 + │ +20 │ emit Governance.Rejected(1, alice); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events + + "#]]); }); diff --git a/crates/lint/README.md b/crates/lint/README.md index 659b34f0cc05f..48bdc49054194 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -3,6 +3,10 @@ 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. +Files under the configured test and script directories are excluded from all lints except +`unsafe-cheatcode`, including when a lint is explicitly selected. The exception still respects +severity filters, exclusions, and inline suppressions. Production source files remain linted. + ## Supported Lints `forge-lint` includes rules across several categories: diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 19d5495c4d412..9ad96ea2490c9 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -130,8 +130,7 @@ impl ForgeLintSuite { .filter(|lint| { self.include_lint(**lint) && path.is_none_or(|path| { - !self.path_config.is_test_or_script(path) - || !matches!(lint.severity(), Severity::Gas | Severity::CodeSize) + !self.path_config.is_test_or_script(path) || lint.id == "unsafe-cheatcode" }) }) .map(|lint| lint.id)