Linter (lint)

September 10, 2026 ยท View on GitHub

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.

Architecture

The forge-lint system operates by analyzing Solidity source code through a dual-pass system:

  1. Parsing: Solidity source files are parsed into an Abstract Syntax Tree (AST) using solar. This AST represents the syntactic structure of the code.
  2. HIR Generation: The AST is then lowered into a High-level Intermediate Representation (HIR) that includes type information and semantic analysis.
  3. Early Lint Passes: The EarlyLintVisitor traverses the AST, invoking registered "early lint passes" (EarlyLintPass implementations) for syntax-level checks.
  4. Late Lint Passes: The LateLintVisitor traverses the HIR, invoking registered "late lint passes" (LateLintPass implementations) for semantic analysis.
  5. Emitting Diagnostics: If a lint pass identifies a violation, it uses the LintContext to emit a diagnostic (either warning or note) that pinpoints the issue. Lints can also provide code fix suggestions through the Suggestion API, which integrates with solar's diagnostic system to support different applicability levels.

Key Components

  • Linter Trait: Defines a generic interface for linters. SolidityLinter is the concrete implementation tailored for Solidity.
  • Lint Trait & SolLint Struct:
    • 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 the Lint trait, 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 (like check_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 (like check_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 registered EarlyLintPass instances.
  • LateLintVisitor<'a, 's, 'hir>: The visitor that traverses the HIR and dispatches checks to the registered LateLintPass instances.
  • Suggestion Struct: Represents code fix suggestions with different kinds (fix or example) and applicability levels, integrated with solar's diagnostic system.

Developing a new lint rule

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>.

  1. Specify an issue that is being addressed in the PR description.
  2. In your PR:
  • Create a static SolLint instance using the declare_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 the mod.rs of 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.

  • Use Solar's Gcx queries for inferred expression types, resolved calls and arguments, and inheritance. Do not reconstruct type-checker results or choose overloads by name or arity. Keep lint-specific data-flow and control-flow analysis in the lint or shared probes.

  • Implement the appropriate trait logic (EarlyLintPass or LateLintPass) for your lint. Do it in a new file within the relevant severity module (e.g., src/sol/med/my_new_lint.rs).

  • Add the canonical documentation at crates/lint/docs/<str_id>.md in the same Foundry PR. Use crates/lint/docs/_template.md as a starting point. The tests in crates/lint/src/sol/mod.rs enforce registry coverage, metadata, help URLs, and the page structure. Run them with cargo test -p forge-lint --lib sol::tests. The Foundry Book validates lint documentation and generates its lint reference pages and navigation from these files with import:lints.

Lint writing style

Follow Clippy's lint authoring guide, diagnostic conventions, and lint naming conventions, with the following Foundry-specific conventions:

  • Use SCREAMING_SNAKE_CASE for the Rust static and kebab-case for its public lint ID. Name new lints after the condition they detect so the name reads naturally when enabled or suppressed. Existing public IDs are configuration and documentation APIs: do not rename them just to adopt Clippy's snake_case spelling or a different naming style.
  • Make the primary diagnostic a short, factual description of the detected problem. Use help messages or suggestion labels for detailed corrective instructions. Start diagnostic text with lowercase prose and omit the final period for a single sentence; preserve capitalization inside code and in acronyms. Use normal sentence punctuation for multi-sentence explanations.
  • Enclose code, identifiers, types, operators, and literal values in backticks in descriptions, labels, notes, and help text. Do not add backticks to replacement source code itself.
  • Highlight the relevant code and make suggestions actionable. Use MachineApplicable only when the replacement is correct without user intervention; document assumptions and limitations.
  • Write each reference page using the documentation contract. Explain what is detected, why it matters, and show a minimal triggering example followed by Use instead: and a corrected example. For policy or style choices, use Why restrict this? rather than claiming that the flagged code is inherently bad. Foundry has severity groups, not a Clippy restriction group, so choose the heading based on the lint's purpose.
  • Keep reference pages user-facing: explain the problem, its impact, and how to address it. Omit lint implementation details such as AST/HIR representation, alias tracking, traversal rules, analysis budgets, diagnostic placement, and comparisons with other detectors. Include a limitation only when it changes how the reader should interpret or address a warning. Implementation explanations belong in developer documentation or source comments.
  • Start reference pages with What it does instead of a duplicate summary. Keep shared lint controls in the linting guide; omit generic review reminders and repeated explanations.

Review every diagnostic path and the reference page together. Cover triggering and non-triggering cases in UI tests, update affected expected output when messages change, and check that examples match the implemented detector. A structural check cannot establish that a suggested change is semantically correct.

Choosing Between Early and Late Passes

  • Use EarlyLintPass for:

    • 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 LateLintPass for:

    • 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

Providing Code Fix Suggestions

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 confidence
  • MaybeIncorrect: The suggestion might not be correct in all cases and should be reviewed
  • HasPlaceholders: The suggestion contains placeholders that need to be filled in
  • Unspecified: No applicability specified
  1. Add comprehensive tests in lint/testdata/:
    • Create MyNewLint.sol with 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.

Testing a lint rule

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: description or //~NOTE: description on 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