Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changelog/lint-test-script-exclusions.md
Original file line number Diff line number Diff line change
@@ -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.
156 changes: 156 additions & 0 deletions crates/forge/tests/cli/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
});

// <https://github.com/foundry-rs/foundry/issues/16662>
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


"#]]);
});

Expand Down
4 changes: 4 additions & 0 deletions crates/lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions crates/lint/src/sol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading