Diagnostics
What this page covers
This page describes how the server produces and publishes diagnostics:
- the two diagnostic sources (
solc/forge buildandforge lint), - how diagnostics are triggered and published per save,
- the content-hash optimization that skips redundant rebuilds,
- how lint is configured and filtered,
- how non-blocking save was implemented,
- what is covered by tests today.
Terms used in this page
forge build: Foundry's build pipeline; used to get AST + compilation errors whensolcdirect mode is off.solc: directsolcinvocation used in--use-solcmode; produces both AST output and diagnostics in one pass, avoiding a separateforge buildrun.forge lint: Foundry's linter (forge lint --json); produces style/best-practice warnings with string codes.publish_diagnostics: the LSP notification sent to the client with the combined diagnostic list.forge-buildsource:sourcefield value for diagnostics coming from the build pipeline.forge-lintsource:sourcefield value for diagnostics coming fromforge lint.- content hash: a
u64hash of the saved file text stored onCachedBuild; used to skip redundant rebuilds when the file bytes are identical to the last build.
Two diagnostic sources
Source 1: solc / forge build (compilation errors)
Compilation diagnostics come from the Solidity compiler output.
In --use-solc mode (default when a compatible solc binary is found):
- A single
solcinvocation produces both the AST and the error list. - Errors are extracted by
build::build_output_to_diagnostics()from the same JSON output used for AST caching. - This avoids running
forge buildseparately (which can take ~27 s on large projects).
In forge-only mode:
compiler.get_build_diagnostics(&uri)runsforge build --jsonand parses theerrorsarray from its output.- Diagnostics are filtered through
build::ignored_error_code_warning()which suppresses codes5574and3860(contract-size / code-size warnings) plus any codes listed infoundry.tomlignored_error_codes.
Diagnostic fields:
source:"forge-build"code: numeric solc error code (e.g.2072)severity: mapped from solc severity string ("error"→ERROR,"warning"→WARNING,"info"→INFORMATION)
Source 2: forge lint
Lint diagnostics come from forge lint --json.
compiler.get_lint_diagnostics(&uri, &lint_settings)runsforge lintand parses the JSON array output.lint::lint_output_to_diagnostics()maps each entry to an LSPDiagnostic:- Only the primary span (
is_primary == true) per diagnostic is used. source:"forge-lint"code: string code fromForgeLintCode.code(e.g."unused-import")severity: mapped fromlevel("error"→ERROR,"warning"→WARNING,"note"→INFORMATION,"help"→HINT)- Empty
messagefields fall back torendered, thenspan.label, then"Lint warning"to avoid crashing clients that require non-empty messages.
- Only the primary span (
- Post-run filtering applies: codes listed in
lint.exclude(editor settings) are removed from the result.
Runtime flow
Diagnostics are produced in on_change, which is called from both did_save and did_open:
Content-hash optimization
Before running any compiler, on_change computes a u64 hash of the saved text using DefaultHasher. If the hash matches CachedBuild.content_hash from the last successful build, the save is a no-op — no solc/forge invocation, no diagnostic publish. This prevents redundant rebuilds in format-on-save loops where a formatter applies edits, the editor saves again, and the resulting bytes are identical to the previous build.
The hash is stored on CachedBuild after each successful build and reset to 0 when the cache is invalidated.
Non-blocking save
did_save delegates work to a background task via tokio::task::spawn_blocking for CPU-heavy blocking operations:
collect_import_pragmas(recursive FS crawl over imported files) runs on the blocking thread pool, not on the async runtime thread. This prevents the async executor from stalling on large projects (~95-file crawls).- The AST save path and diagnostics publish happen on the async runtime after all blocking work is complete.
Lint configuration
Lint behavior is controlled by two layers:
foundry.toml(lint.exclude,lint.severity, etc.) — read at startup and when the workspace config changes.- Editor settings (
settings.lint.*):lint.enabled— master toggle; set tofalseto disable all lint diagnostics.lint.severity—["high", "med", "gas"]maps toforge lint --severityflags.lint.only— maps toforge lint --only-lint.lint.exclude— codes filtered afterforge lintreturns (client-side filtering).
lint_config.should_lint(&file_path) returns false for files outside the project root or in lib/ (dependencies), so third-party code never produces lint diagnostics.
Diagnostic sanitization
Before publishing, all diagnostics (regardless of source) have their message field checked. Any empty message is replaced with "Unknown issue". This prevents crashes in LSP clients (e.g. trunk.io) that require non-empty diagnostic messages.
Main implementation files
| File | Role |
|---|---|
src/lsp.rs | on_change — orchestrates build + lint, hash check, publish |
src/build.rs | build_output_to_diagnostics, ignored_error_code_warning |
src/lint.rs | lint_output_to_diagnostics, ForgeDiagnostic types |
src/solc.rs | solc_ast — combined AST + diagnostics in single invocation |
src/config.rs | LintSettings, LintConfig, should_lint |
Test coverage and confidence
src/build.rs and src/lint.rs have unit tests covering:
ignored_error_code_warningsuppresses codes5574and3860by default.source_location_matchescorrectly matches both absolute and relative forge error paths.lint_output_to_diagnosticsparses a known fixture into the expectedDiagnosticshape.
Recommended explicit additions
- Integration test through
on_change: assert that saving a file with identical bytes skips a rebuild (content-hash short-circuit). - End-to-end test: inject a solc error output fixture and assert
publish_diagnosticsis called with the expectedDiagnosticlist. - Test that
lint.excludecorrectly filters out specified string codes afterforge lintreturns.