Introducing scopelint v0.1.0: Opinionated Linting for Foundry Projects
February 26, 2026 / Aditya Anand
Following Foundry best practices consistently across a team is hard. Naming conventions drift, unused imports pile up, and subtle bugs like mismatched EIP-712 typehash parameters slip through code review unnoticed. Every project has its own interpretation of the Best Practices guide, and without automated enforcement, consistency is a matter of discipline rather than tooling.
scopelint is ScopeLift's opinionated linting and formatting CLI for Foundry projects, built in Rust. It enforces a consistent set of Solidity conventions-test naming, variable casing, error prefixes, and more so your team doesn't have to argue about style in PRs. It also formats your Solidity and TOML files, and can even generate a specification from your test suite.
Install it with:
cargo install scopelint
$ scopelint check
Invalid test name in ./test/Counter.t.sol on line 16: testIncrementBadName
Invalid constant or immutable name in ./src/Counter.sol on line 7: bad_constant
Invalid variable name in ./src/Counter.sol on line 25: Local variable 'x' should have underscore prefix
Invalid error name in ./src/Counter.sol on line 40: Error 'Unauthorized' should be prefixed with 'Counter_'
Unused import in ./src/Counter.sol on line 3: Unused import: 'IERC20'
error: Convention checks failed, see details above
What Does scopelint Do?
scopelint provides four commands:
scopelint fmt: Formats Solidity files using yourfoundry.tomlconfiguration and TOML files with a consistent, opinionated style.scopelint check: Validates that your code follows naming conventions and structural rules.scopelint fix: Auto-fixes what it can (like unused imports), then runscheckfor the rest.scopelint spec: Generates a human-readable specification from your test names, embracing the philosophy that your tests are your spec.
Out of the box, scopelint check enforces conventions that have been around since the early versions:
- Test names must follow
test(Fork)?(Fuzz)?(_Revert(If|When|On|Given))?_Description:
// Valid
function test_Transfer() public {}
function testFuzz_RevertIf_InsufficientBalance() external {}
function testForkFuzz_RevertOn_Condition_MoreInfo() external {}
// Invalid - missing underscore separators
function testTransfer() public {}
function testRevertIfCondition() public {}
- Constants and immutables must use
ALL_CAPS. - Scripts must have exactly one public
runmethod. - Internal/private functions in source files must start with a leading underscore.
Here's what scopelint spec looks like on an ERC-20 token:
$ scopelint spec
Contract Specification: ERC20
├── approve
│ ├── Sets Allowance Mapping To Approved Amount
│ ├── Returns True For Successful Approval
│ └── Emits Approval Event
├── transfer
│ ├── Revert If: Spender Has Insufficient Balance
│ ├── Does Not Change Total Supply
│ ├── Increases Recipient Balance By Sent Amount
│ ├── Decreases Sender Balance By Sent Amount
│ ├── Returns True
│ └── Emits Transfer Event
├── transferFrom
├── permit
└── DOMAIN_SEPARATOR
What's New in v0.1.0
v0.1.0 is a big release. Here's what's changed, grouped by theme.
New Checks
Variable naming conventions (rule: variable) : scopelint now enforces underscore-prefix conventions for variables based on whether a variable access storage or not. Because writing and reading from storage are expensive, sensitive operations, we want to make sure we know when we're doing it. For that reason, all non-storage variables must be prefixed with _
contract MyContract {
uint256 validStateVar; // State variables: no underscore prefix
uint256 _invalidStateVar; // Flagged
function transfer(
uint256 _amount, // Parameters: underscore prefix required
address _to, //
Deposit storage deposit // Storage params: no underscore prefix
) external {
uint256 _localVar = 123; // Local variables: underscore prefix required
Deposit storage ref = deposits[0]; // Local storage refs: no underscore prefix
}
}
Error prefix (rule: error) : Custom errors must be prefixed with their contract name. This prevents naming collisions and makes it immediately clear where an error originates:
contract Vault {
error Vault_InsufficientBalance(); // Valid
error Vault_Unauthorized(address); // Valid
error InsufficientBalance(); // Flagged - missing "Vault_" prefix
}
EIP-712 typehash validation (rule: eip712) : Catches mismatches between the parameter count in a typehash string and its abi.encode usage. This class of bug is particularly insidious because it compiles fine but produces invalid signatures at runtime:
bytes32 constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
// Flagged - typehash defines 5 parameters but abi.encode only uses 3
bytes32 hash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value));
SPDX license header (part of the src rule) : Source files must have a // SPDX-License-Identifier: comment in the file header, before any non-comment code.
Unused imports (rule: import) : Detects unused symbols from named imports (import {A, B} from "...") and unused aliased imports (import "..." as Alias). It's smart enough to recognize @inheritdoc references as valid usage, so documented interface implementations won't produce false positives:
import {IGovernor, Governor, IERC20} from "openzeppelin/Governor.sol";
abstract contract MyGovernor is Governor {
/// @inheritdoc IGovernor // IGovernor counts as used
function hasVoted(...) public view override returns (bool) {}
// IERC20 is never referenced - flagged as unused
}
Here's what these new checks look like in practice:
$ scopelint check
Invalid variable name in ./src/Vault.sol on line 12: State variable '_balance' should NOT have underscore prefix
Invalid variable name in ./src/Vault.sol on line 28: Parameter 'amount' should have underscore prefix
Invalid error name in ./src/Vault.sol on line 8: Error 'Unauthorized' should be prefixed with 'Vault_'
Invalid EIP712 typehash in ./src/Vault.sol: EIP712 typehash 'PERMIT_TYPEHASH' parameter mismatch: typehash defines 5 parameters but abi.encode usage uses 3 parameters
Invalid src method name in ./src/Vault.sol on line 1: Missing SPDX-License-Identifier header
Unused import in ./src/Vault.sol on line 3: Unused import: 'IERC20'
error: Convention checks failed, see details above
New Commands & Flags
scopelint fix : A new command that auto-removes unused imports, then runs check for everything else. Only findings that aren't ignored (via inline comments or .scopelint) are fixed. This is the recommended first step when adopting scopelint on an existing codebase.
scopelint fmt --check : A dry-run mode that shows what would change without modifying files. Useful for CI pipelines where you want to fail the build on unformatted code rather than silently reformat it.
scopelint spec --show-internal : Include internal and private functions in the generated specification. By default, only public and external functions are shown.
$ scopelint fix
info: Fixed unused imports in 3 file(s)
Invalid variable name in ./src/Vault.sol on line 28: Parameter 'amount' should have underscore prefix
Invalid error name in ./src/Vault.sol on line 8: Error 'Unauthorized' should be prefixed with 'Vault_'
error: Convention checks failed, see details above
Configuration & Flexibility
scopelint is opinionated by design and you can't disable rules globally. But sometimes you need escape hatches for specific files or lines. v0.1.0 adds two flexible mechanisms:
Inline ignore directives Add comments directly in your Solidity files to suppress specific rules:
// scopelint: ignore-error-next-line
error LegacyError(); // Not flagged
// scopelint: ignore-variable-start
uint256 legacyVar = 123; // Not flagged
uint256 anotherLegacyVar = 456; // Not flagged
// scopelint: ignore-variable-end
// scopelint: ignore-src-file // Ignore 'src' rule for entire file
Supported scopes: -next-line, -line, -start/-end, -file. Supported rules: error, import, variable, constant, test, script, src, eip712.
.scopelint config file : A TOML file in your project root for file-level and rule-level overrides using glob patterns:
# Ignore entire files from all checks
[ignore]
files = [
"src/legacy/old.sol",
"test/integration/*.sol"
]
# Ignore specific rules for specific files
[ignore.overrides]
"src/BaseBridgeReceiver.sol" = ["src"]
"src/legacy/**/*.sol" = ["src", "error"]
foundry.toml path integration : scopelint reads src, test, and script paths from your foundry.toml, so non-default layouts (e.g. contracts/ instead of src/) work without extra configuration. You can also override paths for scopelint specifically with a [check] section.
Compatibility Improvements
- Transient keyword support : Solidity's
transientstorage keyword is now stripped before parsing so it doesn't cause parse failures. - Interface definitions in scripts : Scripts containing interface definitions no longer trigger false positives.
- Handler file support : Handler files (used in invariant testing) are recognized and validated appropriately.
@inheritdocawareness : Symbols referenced in@inheritdocNatSpec directives are recognized as used, preventing false "unused import" findings.- Graceful handling of missing folders : If configured paths don't exist yet, scopelint skips them instead of erroring.
Getting Started
cargo install scopelint
If you're adding scopelint to an existing project, expect the new validators to flag existing code. Run scopelint fix first to auto-remove unused imports, then work through the remaining scopelint check findings. Use inline ignores or the .scopelint config for intentional exceptions.
For contributors interested in the scopelint codebase itself, see the Development Guide. For bugs, suggestions, or feature requests, open an issue.
What's Next
scopelint spec is still evolving and we're actively looking for feedback on how to make the generated specifications more useful for different audiences.
scopelint is built and maintained by ScopeLift, a team that builds custom smart contract and protocol development solutions.