Skip to content

feat(lint): add 11 Racket, Emacs Lisp and Clojure depth rules - #105

Merged
takeokunn merged 1 commit into
mainfrom
feat/lint-dialect-depth
Aug 3, 2026
Merged

feat(lint): add 11 Racket, Emacs Lisp and Clojure depth rules#105
takeokunn merged 1 commit into
mainfrom
feat/lint-dialect-depth

Conversation

@takeokunn

Copy link
Copy Markdown
Collaborator

RULE_COUNT 334 → 345. Four standalone commands (the Clojure rules); Racket and Emacs Lisp are registry-only.

package rules
lint-racket-depth (5) racket-match-unreachable-clause (Error), racket-for-comprehension-value-discarded, racket-begin0-single-form (fixable), racket-case-lambda-single-clause (fixable), racket-parameterize-empty-bindings
lint-elisp-depth (2) elisp-process-filter-assumes-whole-output, elisp-repeating-timer-handle-discarded
lint-clojure-depth (4, all Error) go-block-blocking-channel-op, parking-op-outside-go-machinery, contains-on-non-associative, reference-type-operator-mismatch

All three batches dropped most of what they proposed, and the refutations are the useful part.

Racket — corpus and toolchain obtained, so every premise was executed

4492 .rkt files and Racket v9.2. The earlier "3 unique .rkt files in the whole store" no longer applies, so nothing here is labelled unaudited.

  • Typed Racket's Any does the opposite of what was proposed. (needs-number (takes-any 5)) is a type errorexpected: Number given: Any. Any maximally constrains consumers, and (-> _ Any) is idiomatic for side-effecting functions. That's the second time a Typed Racket shape-match premise has been refuted in this project.
  • module+ reads a later define fine — it's module* with #f, declared after the body. Printed 6.
  • define/contract on a non-exported function still guarantees every internal caller — the boundary is definition-vs-rest-of-module. Verified: a same-module call raised while the recursive call passed -5 unchecked.
  • Opaque-struct equal? is identity as claimed, but gen:equal+hash also fixes it and 1460 of 1845 corpus structs are deliberately opaque — flagging them is noise.

racket-contract-out-arity-mismatch was built, tested, corpus-audited and then deleted on measurement: 1.25 ms for one realistic file, ~40,000× the control, super-linear — and zero findings over 214 contract-out occurrences, so the cost bought nothing. The evidence and a WholeTree rebuild path are preserved in src/cost_tests.rs.

Corpus: 8 findings, 0 false positives after one was found and fixed — Typed Racket's legacy (case-lambda (Number * -> Number)) type spelling. Two true positives sit in Racket's own collects/setup/parallel-build.rkt.

Emacs Lisp shipped 2 of 8, and that's the honest number

The process-filter bug is real and explains why it ships. A bulk writer emitting 10000 lines in one write reached the filter as 8 invocations with 7 mid-line splits and 7 malformed lines. read-process-output-max is 65536 — so the defect passes every small-output test and corrupts data past 64 KiB. (The first probe used 400 separate printfs and showed 0 splits; the producer was line-aligned, which is why it was re-run.)

Refuted: goto-char clamps rather than signalling ((goto-char 999) in a 6-char buffer → point now 6); advice-remove compares with equal, so a lambda is removable; and setq on a buffer-local variable creates a buffer-local binding — which had already been refuted once in this project and I re-proposed it anyway.

Two more rules were built, audited and deleted rather than shipped at 11% and 20% fire rates on GNU Emacs's own tree: narrowing (112/1044 — rmail, gnus and ediff narrow as their display model) and display properties (500/2679 — font-lock-ensure leaves buffer-modified-p nil, so 'face writes during fontification are correct).

Clojure — one rule survived a check that could have killed it

core.async does ship a blocking-op detector, but check-blocking-in-dispatch fires only under a system property read once at namespace-load time, which its own docs call development-only and "covers only part of the problem". go-impl does no static inspection. So unlike CL loop's collect x sum x, this is not already-caught.

Conversely defrecord-implements-protocol-method-not-in-protocol is a compile error — Compiler.java:9128 throws "Can't define method not in interfaces" — so it died. A no-init reduce rule died on measurement: 35 of 172 corpus occurrences pass a literal fn with no 0-arity, all working code. And dynamic-scope-returns-lazy-seq was built, corpus-run at 978 candidates / 1 finding / 0 true positives, and withdrawn.

The one true positive is onyx/src/onyx/api.clj:723 — a blocking <!! inside a go, re-entered per log entry.

Wiring notes

Two parser gaps found, reported not fixed

  • Racket: 1777 of 4492 files (39.6%) fail to parse#rx/#px (459), #%… (~290), #'/#` (267), #<< here-strings (195), #hash (105), #"…" (81). This caps what any Racket rule in this workspace can see.
  • Emacs Lisp: 191 of 1674 (11.4%), of which 153 are radix literals#x010101, #b01111110, #o777, #24r1k — in ordinary files like bookmark.el, ansi-color.el, bindings.el and calc.el.

Verification

cargo build --workspace, cargo test --workspace (163 suites), cargo test --test cli (3085 passed), cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings — all exit 0.

All 11 fire through the real binary, each reporting only itself. Sweeping tests/fixtures/** gives 0 findings; widened to all 40 repo Lisp files gives 5, all in lint-racket-depth's own dangerous.rkt positive-control fixture, exactly one per Racket rule, with its clean.rkt silent.

Two stale one-line description fields in the copied Cargo.tomls are flagged but not fixed — both READMEs document the dropped rules correctly; only the package blurbs drifted.

RULE_COUNT 334 -> 345. Four standalone commands (the Clojure rules);
Racket and Emacs Lisp are registry-only.

lint-racket-depth (5): racket-match-unreachable-clause (Error),
racket-for-comprehension-value-discarded, racket-begin0-single-form
(fixable), racket-case-lambda-single-clause (fixable),
racket-parameterize-empty-bindings.

lint-elisp-depth (2): elisp-process-filter-assumes-whole-output,
elisp-repeating-timer-handle-discarded.

lint-clojure-depth (4, all Error): go-block-blocking-channel-op,
parking-op-outside-go-machinery, contains-on-non-associative,
reference-type-operator-mismatch.

All three batches dropped most of what they proposed, and the
refutations are the useful part.

**Racket** got both a corpus and a toolchain -- 4492 .rkt files and
Racket v9.2 -- so every premise was executed. Typed Racket's `Any`
does the **opposite** of what was proposed: `(needs-number (takes-any
5))` is a type error, `expected: Number given: Any`, so `Any`
maximally *constrains* consumers and `(-> _ Any)` is idiomatic for
side-effecting functions. That is the second time a Typed Racket
shape-match premise has been refuted here. `module+` reads a *later*
`define` fine, because it is `module*` with `#f`, declared after the
body. `define/contract` on a non-exported function still guarantees
every internal caller -- the boundary is definition-vs-rest-of-module,
verified by a same-module call raising while the recursive call did
not.

`racket-contract-out-arity-mismatch` was built, tested, corpus-audited
and then **deleted on measurement**: 1.25ms for one realistic file,
~40000x the control, super-linear -- and zero findings over 214
`contract-out` occurrences, so the cost bought nothing.

**Emacs Lisp shipped 2 of 8, and that is the honest number.** A bulk
writer proved the process-filter bug: 10000 lines in one write reached
the filter as 8 invocations with **7 mid-line splits**, because
`read-process-output-max` is 65536 -- so the defect passes every
small-output test and corrupts data past 64 KiB. Refuted: `goto-char`
*clamps* rather than signalling; `advice-remove` compares with `equal`
so a lambda **is** removable; and `setq` on a buffer-local variable
creates a buffer-local binding, which had already been refuted once in
this project and was re-proposed anyway.

Two more elisp rules were built, audited and deleted rather than
shipped at 11% and 20% fire rates on GNU Emacs's own tree: narrowing
(112/1044 -- rmail, gnus and ediff narrow as their *display model*) and
display properties (500/2679 -- `font-lock-ensure` leaves
`buffer-modified-p` nil, so `'face` writes during fontification are
correct).

**Clojure**'s `go`-block rule survived a check that could have killed
it: core.async *does* ship a blocking-op detector, but
`check-blocking-in-dispatch` fires only under a system property read
once at namespace load, which its own docs call development-only and
"covers only part of the problem". `go-impl` does no static inspection.
Conversely `defrecord-implements-protocol-method-not-in-protocol` **is**
a compile error -- `Compiler.java:9128` throws "Can't define method not
in interfaces" -- so that rule died. And a no-init `reduce` rule died on
measurement: 35 of 172 corpus occurrences pass a literal fn with no
0-arity, all working code.

`dynamic-scope-returns-lazy-seq` was built, corpus-run at 978
candidates / 1 finding / **0 true positives**, and withdrawn.

Wiring notes:

- **`RULE_DOCS` had to become `static`.** The 345th rule crossed
  clippy's default 16 KiB `array-size-threshold` (345 x 48 = 16560 B;
  at 334 it was 16032, just under). Took clippy's own suggestion rather
  than an `#[allow]`: nothing reads it in a const context, so `static`
  is also the substantive fix, since a `const` is substituted into every
  use site. That threshold is now permanently crossed.
- The two `Fixable` Racket rules needed a **Racket** fixture in
  `fixable_rules_match_the_fix_engine` -- the Scheme fixture does not
  reach them, because `lint-scheme-idiom` declares `[Scheme, Racket]`
  while these declare `[Racket]` alone. Proved by removing the line and
  watching exactly those two go missing. Third time this test has caught
  a `--fix` that would silently do nothing.
- treefmt reformatted `lint-racket-depth`'s own fixtures: the
  `tests/fixtures/*` exclusion is root-anchored and does not cover
  `packages/feature/*/tests/fixtures/`. Same trap as PR #88.
- `docs/src/reference/architecture.md` had drifted across PRs #97, #99,
  #101 and #102 -- it claimed 320 rules and 66 packages. Rewritten with
  measured values.

Two parser gaps found and reported, not fixed. **Racket: 1777 of 4492
files (39.6%) fail to parse** on `#rx`/`#px`, `#%kernel`, `#'`/`` #` ``,
`#<<` here-strings, `#hash`, `#"..."` -- which caps what any Racket rule
in this workspace can see. **Emacs Lisp: 191 of 1674 (11.4%)**, 153 of
them radix literals (`#x010101`, `#b01111110`, `#o777`, `#24r1k`) in
ordinary files like `bookmark.el`, `ansi-color.el` and `calc.el`.
@takeokunn
takeokunn merged commit 1a01636 into main Aug 3, 2026
9 of 10 checks passed
@takeokunn
takeokunn deleted the feat/lint-dialect-depth branch August 3, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant