Racket Linter v0.2.0
1 Quick Start
2 Configuration
3 Command Options
4 Rule Inventory
5 Suppressions and Baselines
6 Abstract Evaluation
7 Auto-Fix
8 Output Formats
9 Core API
10 Testing and Reliability
11 Known Limitations
12 Future Quality Checks
12.1 General Linter Lessons
12.2 S-Expression Opportunities
13 Custom Rules
14 License
9.3.0.2

Racket Linter v0.2.0🔗ℹ

kimmy

 (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

    2 Configuration

    3 Command Options

    4 Rule Inventory

    5 Suppressions and Baselines

    6 Abstract Evaluation

    7 Auto-Fix

    8 Output Formats

    9 Core API

    10 Testing and Reliability

    11 Known Limitations

    12 Future Quality Checks

      12.1 General Linter Lessons

      12.2 S-Expression Opportunities

    13 Custom Rules

    14 License

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