Racket Linter v0.2.0
| (require racket-linter) | package: racket-linter |
Racket Linter is a configurable, extensible static analysis tool for Racket projects. It scans *.rkt files, runs text, syntax, and expansion rules, and reports file-level and project-level diagnostics.
The Scribble manual is the source of truth for the command and rule contract. The repository README contains only a short development quick start.
1 Quick Start
Install the package or link a checkout:
raco pkg install /path/to/racket-linter |
raco pkg install --link /path/to/racket-linter |
Re-index a linked checkout after changing info.rkt or command metadata:
raco setup --pkgs racket-linter |
Run the command:
raco lint /path/to/project |
raco lint --help |
raco lint --output json /path/to/project |
The command exits with status 0 when no diagnostics are produced and status 1 when at least one diagnostic is produced. Invalid command-line arguments and internal rule failures also return a non-zero status.
2 Configuration
Create .racket-linter.rkt in the project root. The file may be a normal Racket module with a #lang line and must evaluate to a hash:
#lang racket/base |
(hash |
'style/line-length (hash 'max-length 120) |
'reachability/unused-require (hash 'enabled #t) |
'export/unused-project (hash 'enabled #f)) |
For compatibility, a configuration file containing only the hash expression is also accepted. User configuration is merged with each rule’s defaults. Project-level diagnostics use the same rule IDs and configuration hash.
Configuration is evaluated as trusted Racket code. Do not load an untrusted project configuration without sandboxing or reviewing it first.
3 Command Options
--help prints usage and exits successfully.
--fix applies safe fixes after applicability and idempotence checks.
--fix-preview prints safe replacement previews to stderr without writing files.
--format runs raco fmt on discovered files.
--no-config ignores the project configuration.
--config <file> selects a configuration file.
--exclude <directory> excludes matching paths; it can be repeated.
--parallel analyzes files concurrently and collects results in file order.
--output <text|json|sarif|junit> selects the output format.
--baseline <file> suppresses exact diagnostics recorded in a baseline.
--write-baseline <file> writes the current unsuppressed diagnostics as a versioned baseline.
Only one project directory argument is accepted. JSON, SARIF, and JUnit output are machine-readable; all strings are escaped by their respective serializers.
4 Rule Inventory
The following table describes the rules registered by the CLI. Rules marked enabled run unless disabled by configuration. Rules marked disabled are available but opt-in.
Rule ID | Layer | Default | Contract |
style/line-length | text | enabled | Reports lines over configurable max-length; default 102 |
style/trailing-whitespace | text | enabled | Reports trailing spaces or tabs |
style/newline-at-eof | text | enabled | Requires a final newline |
style/sexpr-depth | syntax | disabled | Reports syntax nesting over configurable max-depth; default 10 |
style/definition-length | text | enabled | Reports definitions over 66 lines |
style/file-length | text | enabled | Reports files over 1000 lines |
style/naming-convention | text | disabled | Reports underscores and camelCase |
style/require-sort | syntax | disabled | Syntax-aware phase/module ordering for require specs |
style/provide-sort | syntax | disabled | Syntax-aware ordering for provide specs |
style/extract-let | text | disabled | Suggests extracting repeated expressions |
style/simplify-cond | syntax | disabled | Inspects cond clauses and suggests else/if simplifications |
definition/unused | syntax | disabled | check-syntax-backed lexical unused-definition diagnostics |
reachability/undefined | syntax | disabled | Reports references not resolved by the local scanner |
reachability/unused-require | syntax | disabled | Reports unused required bindings using syntax scanning |
reachability/unused-require-expand | expand | disabled | Reports unused requires after expansion |
export/unused | syntax | disabled | Reports exports not used within one module |
module/require-provide | syntax | disabled | Reports provided names without local definitions |
abstract/type-error | expand | disabled | Conservative definite non-procedure application checks |
abstract/unreachable-code | text | disabled | Heuristic scan for code after exit, raise, or error |
check-syntax/unused | syntax | disabled | Uses DrRacket binding identity for unused binders and requires |
review/syntax-quality | syntax | disabled | Syntax-aware raco-review-compatible binding and form-shape checks |
review/module-declaration | text | disabled | Reports a missing #lang module declaration |
review/raco-review | text | disabled | Optional bridge to the installed raco-review rule implementation |
module/circular-dependency | project | enabled | Reports cycles in the simplified require graph |
module/phase-parse | project | disabled | Reports source/module graph parse failures |
module/phase-unresolved-require | project | disabled | Reports unresolved relative requires with phase |
module/phase-cycle | project | disabled | Reports phase-aware dependency cycles |
export/unused-project | project | disabled | Reports exports unused by files in this project |
The project-level export rule cannot know about consumers outside the scanned project. Library projects should normally disable it or use a project-specific entry-point policy.
5 Suppressions and Baselines
A source suppression must name one or more registered rule IDs. The line and next-line forms affect only the diagnostic starting on that line; the range forms affect subsequent lines until enabled again:
; racket-linter-disable-line style/line-length |
; racket-linter-disable-next-line style/line-length |
; racket-linter-disable style/line-length |
; racket-linter-enable style/line-length |
Unknown IDs, malformed directives, and enables without an active disable are reported as errors. Suppression policy diagnostics cannot be suppressed by the same source file.
Use --write-baseline to create a JSON baseline and --baseline to consume it in CI. Each entry records a project-relative path, rule ID, one-based line, zero-based column, and SHA-1 hash of the diagnostic message. A finding is suppressed only when all of those values match. Changed findings are therefore reported, and entries with no current finding emit a warning with rule ID baseline/stale-entry. Baselines are generated after source suppressions have been validated and applied.
Rules declare one of these layers:
text receives raw file text and runs for every file.
syntax receives syntax only for languages in the safe-language whitelist. Syntax-aware rules should use this source-preserving tree instead of reparsing raw text.
expand receives expanded syntax only for safe languages. Read, syntax, and expansion failures become diagnostics and are never converted into an empty success result.
check-syntax/unused and core/check-syntax retain lexical definition/reference facts from DrRacket’s traversal.
review/raco-review is an opt-in compatibility backend and requires the review package to be installed.
both runs in both the text and syntax phases.
project is implemented by the project analysis pass and receives the discovered file set.
Non-whitelisted languages are analyzed by text rules only. This is intentional: expansion can load modules and execute compile-time code.
6 Abstract Evaluation
analyze-abstract accepts expanded syntax and a source path, and returns a list of diagnostics.
The abstract domain includes top/bottom, scalar values, lists/pairs, vectors, multiple values, procedures with arity, unions, and lexical closure environments.
It detects definite non-procedure applications, known arity failures, numeric/list/vector contract failures, and constant-branch reachability cases.
The evaluator uses a bounded recursive-binding approximation and lexical identifier identity. It is not a Racket type checker and does not prove general program properties. Unknown values are represented by top and should not produce a definite type diagnostic. The separate abstract/unreachable-code rule is currently a text heuristic; it is not a proof generated by the abstract interpreter.
7 Auto-Fix
Supported fixes are intentionally limited:
trailing whitespace removal with source-line applicability checks
missing final newline insertion with EOF applicability checks
final #t to else replacement only when the syntax span and diagnostic agree
--fix applies only fixes that pass applicability and idempotence checks. --fix-preview prints each replacement to stderr without writing. Require, provide, extract-let, and other heuristic transformations remain diagnostics only until they have syntax spans and a safe replacement contract.
8 Output Formats
raco lint --output json /path/to/project |
raco lint --output sarif /path/to/project |
raco lint --output junit /path/to/project |
JSON is an object containing a diagnostics array. SARIF uses version 2.1.0 with one run. JUnit emits one testcase per diagnostic. All three formats are serialized structurally rather than assembled from unescaped strings.
9 Core API
The public core modules provide these values:
diagnostic, diagnostic?, and diagnostic accessors for locations and messages.
suppression-index, read-suppressions, and diagnostic-suppressed? for validated source directives.
baseline-entry, read-baseline, write-baseline!, and apply-baseline for exact diagnostic baselines.
rule, rule?, define-rule, and rule accessors for rule registration.
run-file for file-level rule execution.
merge-configs for recursive default/user configuration merging.
analyze-project, build-dependency-graph, and project diagnostics.
check-syntax-facts returns definitions, lexical references, unused binder/require spans, and analysis errors.
parse-module-facts, build-phase-module-graph, and check-phase-module-graph for phase-aware module facts and diagnostics.
analyze-abstract for conservative expanded-syntax analysis.
A rule check receives syntax or #f, a path string, and its merged configuration hash, and returns a list of diagnostics. The CLI adds an linter/internal-error diagnostic when a rule raises an exception.
10 Testing and Reliability
Run the package tests after changing rules or the engine:
raco test tests |
raco setup --pkgs racket-linter |
raco lint --no-config --output json /path/to/a/fixture-project |
Rule tests should assert the diagnostic rule ID, severity, location, and message for positive cases, and explicitly assert zero diagnostics for valid cases. Tests that only assert that a result is a list do not establish rule correctness. Expansion tests must distinguish a valid no-diagnostic result from an expansion failure.
11 Known Limitations
The undefined-identifier rule uses expansion/check-syntax failure information where available; its remaining local scanner is heuristic and opt-in.
The optional review/raco-review bridge depends on the installed review package and preserves its version-specific behavior.
Project export analysis cannot observe library consumers outside the scanned directory.
The abstract interpreter is conservative and incomplete; it is not a full type system or theorem prover.
The unreachable-code rule is text-based and should be treated as a heuristic.
The check-syntax adapter depends on DrRacket APIs and may not expose every binding diagnostic.
The simplified require/provide graph does not fully model phases, submodules, collection resolution, or dynamic requires.
Suppressions currently target a diagnostic’s starting line; end spans are not yet part of the diagnostic contract.
Baselines use message hashes, so intentional message changes require regenerating the baseline.
Configuration evaluation is trusted-code execution.
Auto-fixes are limited to applicability-checked, idempotent replacements; use --fix-preview to inspect them without writing.
12 Future Quality Checks
The following backlog is prioritized for code quality, stability, and maintainability rather than raw rule count. The local racket-review test corpus is the compatibility reference for surface checks; the Racket Check Syntax API and syntax/parse are the semantic foundation.
Priority | Capability | Implementation | Value |
P0 | Binding identity and precise source spans | check-syntax facts plus expanded syntax | Removes name-based false positives |
P0 | Review-compatible structural checks | source-preserving syntax walker | Covers malformed/control-shape bugs before runtime |
P0 | Expansion failure visibility | engine result protocol and fixture tests | Prevents false clean CI results |
P1 | Phase-aware require/provide graph | identifier-binding, module resolver, submodule/phase keys | Improves cross-module stability |
P1 | Suppressions and baselines | line/module directives with rule-id validation | Makes adoption practical without hiding failures |
P1 | Safe fixes with applicability checks | syntax spans, replacement previews, idempotence tests | Reduces formatter-induced regressions |
P1 | Complexity and maintainability metrics | syntax counts for nesting, branches, definitions, duplicate forms | Finds code that is hard to review |
P2 | Security and resource checks | literal require paths, dynamic-eval/load, shell/process/network use | Catches risky operations with explicit policy |
P2 | API compatibility and documentation checks | provide/contract/struct signatures plus docs metadata | Protects public library surfaces |
P2 | Test-quality checks | syntax recognition of test-case/check-equal?/check-exn assertions | Detects weak or vacuous tests |
12.1 General Linter Lessons
The most useful features to borrow from mature tools such as Clippy, Ruff, ESLint, and ShellCheck are stable diagnostic identity, configuration profiles, ignore directives that name a rule, machine-readable output, deterministic parallel execution, fix previews, and tests that assert both positive and negative examples. Baseline files should record an exact rule ID, source span, and message fingerprint; a bare line-based ignore is too easy to hide regressions.
Rules should be split into definite errors, high-confidence warnings, and advisory information. A rule that depends on heuristics should default to opt-in or emit an advisory level. Every fix should be idempotent, preserve source spans where possible, and have a no-op check after application.
12.2 S-Expression Opportunities
S-expressions make several high-value checks inexpensive and reliable without expansion: delimiter/paren-shape consistency, empty bodies, branch arity, duplicate binding names in one scope, shadowing, ‘cond‘ fallthrough, nested ‘if‘ shape, ‘match‘ fallthrough and impossible literal patterns, ‘case‘ quoted constants, ‘for‘ clause scopes, require/provide phase ordering, suspicious quoted code, repeated literal expressions, and literal conditions such as ‘(if #t ...)‘. These should be source-syntax rules first and expanded rules only when binding identity or macro semantics is needed.
The next implementation sequence is therefore: complete the phase-aware binding graph, finish the remaining ‘raco review‘ structural corpus, add validated suppressions/baselines and safe fixes, then add complexity/security/ test-quality profiles behind explicit configuration.
13 Custom Rules
A custom rule module can export a custom-rules list:
#lang racket/base |
(require racket-linter/core/rule |
racket-linter/core/diagnostic) |
|
(define-rule my/custom-rule |
#:id 'my/custom-rule |
#:severity 'warning |
#:config-keys (hash 'enabled #t) |
#:layer 'text |
(lambda (stx path config) |
'())) |
|
(provide custom-rules) |
(define custom-rules (list my/custom-rule)) |
14 License
MIT