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.
The forge-lint system operates by analyzing Solidity source code through a dual-pass system:
- Parsing: Solidity source files are parsed into an Abstract Syntax Tree (AST) using
solar. This AST represents the syntactic structure of the code. - HIR Generation: The AST is then lowered into a High-level Intermediate Representation (HIR) that includes type information and semantic analysis.
- Early Lint Passes: The
EarlyLintVisitortraverses the AST, invoking registered "early lint passes" (EarlyLintPassimplementations) for syntax-level checks. - Late Lint Passes: The
LateLintVisitortraverses the HIR, invoking registered "late lint passes" (LateLintPassimplementations) for semantic analysis. - Emitting Diagnostics: If a lint pass identifies a violation, it uses the
LintContextto emit a diagnostic (eitherwarningornote) that pinpoints the issue. Lints can also provide code fix suggestions through theSuggestionAPI, which integrates with solar's diagnostic system to support different applicability levels.
LinterTrait: Defines a generic interface for linters.SolidityLinteris the concrete implementation tailored for Solidity.LintTrait &SolLintStruct: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 theLinttrait, 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 (likecheck_expr,check_item_function, etc.) called by the AST visitor.LateLintPass<'hir>Trait: Lints that require type information and semantic analysis implement this trait. It contains methods (likecheck_contract,check_function, etc.) called by the HIR visitor.LintContext<'s>: Provides contextual information to lint passes during execution, such as access to the session for emitting diagnostics and methods for emitting suggestions.EarlyLintVisitor<'a, 's, 'ast>: The visitor that traverses the AST and dispatches checks to the registeredEarlyLintPassinstances.LateLintVisitor<'a, 's, 'hir>: The visitor that traverses the HIR and dispatches checks to the registeredLateLintPassinstances.SuggestionStruct: Represents code fix suggestions with different kinds (fix or example) and applicability levels, integrated with solar's diagnostic system.
We recommend you start by writing out some Solidity code that you want to trigger a lint in crates/lint/testdata. Name the file after your lint rule.
Next, choose whether you want an early or late lint pass. If your lint is early, you can use Solar to dump the AST and find the patterns you need to match on in your lint code using solar -Zdump=ast crates/lint/testdata/<file.sol>. If your lint is late, you can use solar -Zdump=hir crates/lint/testdata/<file.sol>.
- Specify an issue that is being addressed in the PR description.
- In your PR:
-
Create a static
SolLintinstance using thedeclare_forge_lint!to define its metadata.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 ); // Note: The macro automatically generates a help link to the Foundry book
-
Declare the lint module and register its pass(es) with
register_lints!in themod.rsof its corresponding severity category. Entries are grouped by module (module: (PassStruct, early | late | project, (LINTS...)), ...;); a single pass can handle multiple lints and a module can declare several passes:mod mixed_case; mod pascal_case; mod screaming_snake_case; register_lints!( pascal_case: (PascalCaseStruct, early, (PASCAL_CASE_STRUCT)); mixed_case: (MixedCaseVariable, early, (MIXED_CASE_VARIABLE)), (MixedCaseFunction, early, (MIXED_CASE_FUNCTION)); screaming_snake_case: (ScreamingSnakeCase, early, (SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE)); ); // The macro glob-imports each module and generates the pass structs, `REGISTERED_LINTS` and // the registration function.
-
Reuse the shared HIR probes in
crates/lint/src/sol/analysis/(expression, statement, type and access-control helpers) instead of reimplementing them in the lint. -
Implement the appropriate trait logic (
EarlyLintPassorLateLintPass) for your lint. Do it in a new file within the relevant severity module (e.g.,src/sol/med/my_new_lint.rs). -
Add a markdown documentation file for the lint at
crates/lint/docs/<str_id>.md. The file is referenced by the lint'shelpURL (https://getfoundry.sh/forge/linting/<str_id>) and is consumed by the Foundry book to render the lint reference page. Usecrates/lint/docs/_template.mdas a starting point. The presence of this file is enforced by theregistered_lints_have_docsunit test incrates/lint/src/sol/mod.rs.
-
Use
EarlyLintPassfor:- Syntax-level checks (naming conventions, formatting)
- Simple pattern matching that doesn't require type information
- Lints that can be determined from the AST alone
-
Use
LateLintPassfor:- Semantic analysis requiring type information
- Cross-reference checks between different parts of the code
- Complex patterns that need to understand the actual behavior
- Avoiding false positives through type-aware analysis
Lints can provide actionable code fix suggestions using the emit_with_suggestion method. The Suggestion API integrates with solar's diagnostic system and supports different applicability levels:
use solar::interface::diagnostics::Applicability;
// Example: Suggesting a machine-applicable fix
cx.emit_with_suggestion(
lint,
node.span,
Suggestion::fix(
corrected_name,
Applicability::MachineApplicable,
)
.with_desc("consider using")
);
// Example: Suggesting a fix with a specific span
cx.emit_with_suggestion(
lint,
node.span,
Suggestion::fix(
optimized_code,
Applicability::MaybeIncorrect,
)
.with_desc("use inline assembly for gas optimization")
.with_span(replacement_span)
);
// Example: Providing an example (non-applicable suggestion)
cx.emit_with_suggestion(
lint,
node.span,
Suggestion::example("some example")
);Applicability Levels:
MachineApplicable: The suggestion can be applied automatically with high confidenceMaybeIncorrect: The suggestion might not be correct in all cases and should be reviewedHasPlaceholders: The suggestion contains placeholders that need to be filled inUnspecified: No applicability specified
- Add comprehensive tests in
lint/testdata/:- Create
MyNewLint.solwith various examples (triggering and non-triggering cases, edge cases). - If your test requires imports, add those files under
lint/testdata/auxiliary/so that the ui runner doesn't lint them. - Generate the corresponding blessed file with the expected output.
- Create
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: descriptionor//~NOTE: descriptionon 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:
// 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 / bless (re-generate) the output files:
// using the default cargo cmd for running tests cargo bless-lints