feat(lint): add 10 Fennel, Janet and CL declaration rules - #89
Merged
Conversation
takeokunn
force-pushed
the
feat/lint-batch-r8
branch
from
August 3, 2026 07:19
dc484a2 to
afc6ca2
Compare
RULE_COUNT 303 -> 313. Both packages are registry-only, so no new commands and the dialect-matrix lists do not move. lint-fennel-janet-idiom (5): var-never-set (Fennel and Janet), fennel-deprecated-form, fennel-each-over-non-iterator, janet-empty-loop-body, janet-mutating-immutable-literal. Every rule here is grounded in the language's own tooling rather than in taste: Fennel ships a linter plugin asserting exactly `var-never-set`, and Janet's boot.janet has `check-empty-body` calling `maclintf` on the three heads `janet-empty-loop-body` uses. lint-type-declaration (5): declare-not-at-head-of-body, declaim-inside-body, type-declaration-contradicts-initform, the-form-with-impossible-type, type-declaration-on-rest-parameter. Six proposals were dropped against SBCL 2.6.0 rather than shipped: - A late `declaim ftype` is *not* lost -- SBCL flagged a later bad call identically to the early-declaim version, so the rule had nothing to detect. - `(declare (special *x*))` with no defvar compiles silently and is the ordinary way to reference a cross-file special, so it is not soundly decidable at file scope. - `(optimize (safety 0))` was dropped rather than shipped on alarm value: SBCL's own constraint.lisp sets it deliberately. - `ignore-declared-variable-then-used` was a true duplicate of lint-convention's ignore-declaration-conflict, down to the same worked example. Two Fennel/Janet proposals died the same way. `fennel-global-set-without -global` is a compile error, not a lint. `janet-def-shadows-core` would have been this batch's zero-true-positive rule: 71 core-name definitions across 241 files, the majority inside Janet's own core, which the rule cannot distinguish from shadowing it. `janet-string-concat-in-loop` was dropped as a duplicate of lint-performance's quadratic-accumulation after reading its body -- same loop-keyed, self-referencing-accumulator analysis, differing only in vocabulary tables. The right fix there is a dialect table on that rule, not a twin. Corpus audits: 288 .fnl and 241 .janet files from ten third-party projects, and 2217 CL files with 21239 `declare` and 3979 `declaim` occurrences. Both found real false positives and both were fixed. `var-never-set` lost 10 findings to project-local macros expanding to `set`; the type rules lost all 16 of their first-pass findings, to CLHS 3.2.3.1 (a top-level `locally` body *is* a top-level context), reader conditionals as the head shifting every index, `#.` docstrings, and Lisp-2 operator position. Two measurement traps worth recording. A first sweep reported zero findings because an invalid `--emit json` made every batch error out -- a false clean. And `is_unevaluated_at` descends from the file root, so a linear scan there cost one pass over every top-level form per finding: 646ms at 500 reporting definitions, now binary-searched. Carried forward, not fixed here: Janet's backtick long strings are unimplemented in classify_janet, so ~21% of real Janet files get a wrong tree -- 9 fail outright including Janet's own boot.janet, and 41 more parse at exit 0 with phantom nodes inside string bodies. And the shipped `ignore-declaration-conflict` produces 21 apparent false positives over SBCL's sources; corrected guards are in lint-type-declaration's support.rs ready to lift across. Both are recorded in the packages' READMEs and in the registry provenance comment.
A later pass fixed the rule and measured it properly: 45 findings, not 21, and 2 of them are true positives, so "all false positives" was wrong. The four classes recorded here are only 17 of the 43 real ones -- destructuring macro lambda lists (21) and supplied-p variables (5) were missed entirely, and neither is a body-walk bug. The rule could not read a lambda list at all.
takeokunn
force-pushed
the
feat/lint-batch-r8
branch
from
August 3, 2026 07:41
6dd3657 to
0c76a08
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #88 — review that first.
RULE_COUNT303 → 313. Both packages are registry-only, so no new commands and the dialect-matrix lists do not move.lint-fennel-janet-idiomvar-never-set(Fennel and Janet),fennel-deprecated-form,fennel-each-over-non-iterator,janet-empty-loop-body,janet-mutating-immutable-literallint-type-declarationdeclare-not-at-head-of-body,declaim-inside-body,type-declaration-contradicts-initform,the-form-with-impossible-type,type-declaration-on-rest-parameterEvery Fennel/Janet rule is grounded in the language's own tooling rather than in taste: Fennel ships a linter plugin asserting exactly
var-never-set(src/linter.fnl:80-82, pinned by its owntest-var-never-set), and Janet'sboot.janet:626-629hascheck-empty-bodycallingmaclintf "empty loop body"on precisely the three headsjanet-empty-loop-bodyuses.Six proposals dropped against a running implementation
declaim ftypeis not lost. SBCL flagged a later bad call identically to the early-declaim version, so the rule had nothing to detect. The only real effect is the body's own conflict dropping fromWARNINGtoSTYLE-WARNING— which SBCL already reports.(declare (special *x*))with nodefvarcompiles silently and is the ordinary way to reference a cross-file special. Not soundly decidable at file scope.(optimize (safety 0))dropped rather than shipped on alarm value — SBCL's ownconstraint.lispsets it deliberately in alocally, with a comment explaining why.ignore-declared-variable-then-usedwas a true duplicate oflint-convention'signore_declaration_conflict— same diagnosis, same category, sameFixability, literally the same worked example.fennel-global-set-without-globalis a compile error, not a lint (compiler.fnl:676assert-compile … "expected var <x>").janet-def-shadows-corewould have been this batch's zero-true-positive rule — 71 core-name definitions across 241 files, the majority inside Janet's own core, which the rule cannot distinguish from shadowing it.janet-string-concat-in-loopwas dropped as a duplicate oflint-performance'squadratic-accumulationafter reading its body: the same loop-keyed, self-referencing-accumulator analysis, differing only in vocabulary tables. The right fix there is a dialect table on that rule, not a twin here.One premise was refuted upward:
declare-not-at-head-of-bodywas proposed as a warning, but SBCL errors —caught ERROR: There is no function named DECLARE. HenceSeverity::ErrorandMalformed.Corpus audits, and what they cost
288
.fnland 241.janetfiles from ten third-party projects; 2217 CL files with 21239declareand 3979declaimoccurrences. Both found real false positives and both were fixed:var-never-setlost 10 findings to project-local macros that expand toset(tangerine.nvim'sappend!,jpm'ssetfn). Now suppressed via a macro-vocabulary read from the file's own macro bindings, guarded by symmetric controls so the suppression can't widen.locallybody is a top-level context), a reader conditional as the head shifting every child index,#.docstrings, and Lisp-2 operator position.Two measurement traps worth recording. A first sweep reported zero findings because an invalid
--emit jsonmade every batch error out — a false clean. Andis_unevaluated_atdescends from the file root, so a linear scan there cost one pass over every top-level form per finding: 646 ms at 500 reporting definitions, now binary-searched.Carried forward, not fixed here
classify_janet(packages/core/syntax/src/sexpr/reader_policy.rs), so ~21% of real Janet files get a wrong tree — 9 fail to parse outright including Janet's ownsrc/boot/boot.janet, and 41 more parse at exit 0 with phantom nodes inside string bodies. Triple-backtick docstrings are the dominant Janet idiom, so this is not an edge case.ignore-declaration-conflictproduces 21 apparent false positives over SBCL's sources (quoted templates, shadowing, Lisp-2 operator position, macro arguments). Corrected guards are already written inlint-type-declaration'ssupport.rs, ready to lift across.Both are recorded in the packages' READMEs and in the registry provenance comment so they don't get lost.
Verification
cargo build --workspace,cargo test --workspace,cargo test --test cli(3083 passed),cargo fmt --all --check,cargo clippy --all-targets --all-features -- -D warnings— all exit 0.All 10 fire through the real binary, parsing the JSON
findingsarray —var-never-seton both.fnland.janet, confirming its two-dialect scope reaches both. Sweepingtests/fixtures/**produced exactly one new finding and it is a true positive:tests/fixtures/corpus/janet.janet:41(var acc @[]), never reassigned —array/pushmutates the array object, not the binding, sodefis correct. All 12 goldens are insertions only (848 insertions, 0 deletions).Mutation testing: Fennel/Janet 33 of 34 killed (the survivor is a documented pure-performance guard, and one mutation exposed a missing test, now added); type-declaration 8 of 8 live. One guard kills only its own unit test — traced and documented as correct-but-behaviourally-inert rather than quietly deleted.