feat(lint): add 4 Hy and LFE lint rules - #93
Merged
Conversation
RULE_COUNT 316 -> 320, registry-only, no new commands. This completes
dialect coverage: every one of the ten dialects now has at least one
rule written for it specifically.
lint-hy-lfe-idiom: hy-mutable-default-argument (Error),
hy-identity-comparison-with-literal, hy-bare-except, and
lfe-catch-swallows-exit (pedantic).
Both interpreters were available, so every premise was run rather than
argued. Hy 1.3.1 and LFE 2.2.0 / Erlang 27.3.4.15.
Hy's mutable default is the mirror image of a rule this project already
refuted for Common Lisp: CL re-evaluates an `&optional` init form per
call, but Python evaluates a default **once**, at definition time, so
the same shape that is fine in CL is a real bug here. Confirmed: three
calls return [1], [1 1], [1 1 1]; `{}` and `#{}` behave identically and
`None` does not.
Also confirmed: CPython emits `SyntaxWarning: "is" with 'int' literal`
through Hy; `(except [])` catches KeyboardInterrupt and SystemExit while
`(except [e Exception])` catches neither; and `(catch (exit 'boom))`,
`(tuple 'EXIT 'boom)` and `(catch (tuple 'EXIT 'boom))` are the
**identical term**, so an LFE caller cannot tell failure from success.
Refuted and dropped: `(if 'false 'a)` returns false in LFE rather than
raising, so if-without-else is not a defect; `!` to a dead pid returns
the message with no error and is not statically detectable; and
case/receive without a catch-all is "let it crash", which would be mass
false positives.
The corpus audit -- 2825 .hy across 284 repos, 2701 .lfe across 195 --
changed three of the five candidates:
- `hy-mutable-class-attribute` was **killed**: 33 findings, 251
candidates, 0 genuine defects. The premise is true and measured, but
in real Hy a mutable class attribute is a *declaration* (`__slots__`,
Django ModelAdmin fields, Textual BINDINGS), not accidental sharing.
- `hy-mutable-default-argument` was narrowed from "mutable default" to
"mutable default the body mutates", taking it from 88 findings to 1.
The survivor is a genuine bug whose own docstring shows the author
expected a fresh set per call.
- `lfe-catch-swallows-exit` was tagged pedantic rather than killed:
146 findings but only 9 of ~143 repos, and two of the three heaviest
are LFE's own implementation.
Both dialect gates were proven on the corpus, not just in unit tests:
38 `catch` candidates in Hy files produced 0 findings, and 5 `is`
candidates in LFE files produced 0.
The wiring exposed a latent bug in feature_dependency_contract, flagged
not fixed: it scans manifests as whole text, so the package's *comment*
explaining a removed dev-dependency read as two declared feature edges.
Rewording it to name the packages by directory fixed the build -- but an
intermediate wording containing the bare marker `paredit-feature-` made
the scanner emit an empty-string dependency name and fail differently.
It cannot tolerate the marker appearing without a name after it.
Reader limitations found while building this are recorded in the
package README rather than worked around silently: Hy's interpolated
f-strings are unimplemented (390 of 2825 files fail to parse), `#!`
shebangs are not stripped (393 files, exit 0 with junk atoms), Hy
bracket strings `#[[...]]` are parsed as *code* (33 files -- the package
defends against this explicitly, since a rule could otherwise fire on
text that is not code), Hy's `~` is not a ReaderPrefix so QuoteState
never counts down, and LFE `#B(...)`/`#M(...)` are orphaned from their
list in 243 files, inflating arity at exit 0. Same class as the Janet
backtick defect fixed in #91, and a repair belongs in core/syntax.
takeokunn
force-pushed
the
feat/lint-batch-r11
branch
from
August 3, 2026 08:53
34cb4b9 to
c2905c1
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 #92.
RULE_COUNT316 → 320, registry-only, no new commands.This completes dialect coverage — every one of the ten dialects now has at least one rule written for it specifically. Before this session's batches, 13 of 286 rules ran on anything but Common Lisp.
hy-mutable-default-argumenthy-identity-comparison-with-literalhy-bare-exceptlfe-catch-swallows-exitpedanticPremises were run, not argued
Both interpreters were obtainable — Hy 1.3.1 and LFE 2.2.0 / Erlang 27.3.4.15.
The Hy mutable default is the mirror image of a rule this project already refuted for Common Lisp. CL re-evaluates an
&optionalinit form per call, so the shared-mutable-default defect does not exist there; Python evaluates a default once, at definition time, so the identical shape is a real bug in Hy. Confirmed: three calls return[1],[1 1],[1 1 1];{}and#{}behave identically,Nonedoes not.Also confirmed: CPython emits
SyntaxWarning: "is" with 'int' literalthrough Hy;(except [])catchesKeyboardInterruptandSystemExitwhile(except [e Exception])catches neither; and(catch (exit 'boom)),(tuple 'EXIT 'boom)and(catch (tuple 'EXIT 'boom))are the identical term, so an LFE caller cannot tell failure from success.Refuted and dropped:
(if 'false 'a)returnsfalsein LFE rather than raising, so if-without-else is not a defect;!to a dead pid returns the message with no error and isn't statically detectable;case/receivewithout a catch-all is "let it crash" and would be mass false positives.Also stale in the original brief: Hy 1.3.1 rejects
#@decorators andwith-decorator(the current form is(defn [decorator] name …)), and mangling isfoo?→hyx_fooXquestion_markX, not_is_.The corpus audit changed three of five candidates
2825
.hyacross 284 repos, 2701.lfeacross 195 — nobody involved wrote any of it.hy-mutable-default-argumenthy-identity-comparison-with-literalhy-bare-exceptlfe-catch-swallows-exithy-mutable-class-attributewas killed — 33 findings over 251 candidates, 0 genuine defects. The premise is true and I measured it ((is a.items b.items)really isTrue), but in real Hy a mutable class attribute is a declaration:__slots__,__all__, DjangoModelAdminfields, TextualBINDINGS, Gymmetadata. A test pins the decision so it isn't reintroduced.hy-mutable-default-argumentwas narrowed from "mutable default" to "mutable default the body mutates", taking it from 88 findings to 1. The dropped ones were hand-checked —[notes [0 5 7]],[breaks ["amen.wav" …]]are read-only lookup tables. The survivor is genuine:[known-ids #{}]with(.add known-ids id)in a body whose own docstring shows the author expected a fresh set per call.lfe-catch-swallows-exitwas taggedpedanticrather than killed. 146 findings, but only 9 of ~143 repos and two of the three heaviest are LFE's own implementation.Both dialect gates were proven on the corpus, not just in unit tests: 38
catchcandidates in Hy files → 0 findings; 5iscandidates in LFE files → 0.The pedantic tag makes the preset arithmetic non-obvious
The divisor moves too, so none of these is "previous + 4":
rule_count301 → 304 = 320 − 16 (was 316 − 15). Rises by 3.ReportOnly.Confirmed independently in the regenerated goldens:
broad.json.goldengained exactly three rule rows, not four.A latent contract bug this exposed, flagged not fixed
feature_dependency_contractscans manifests as whole text rather than parsing them, so the package's comment explaining a removed dev-dependency read as two declared feature→feature edges. Rewording it to name the packages by directory fixed the build — no allowlist entry was added, since the edges don't exist and that would have weakened the contract.But an intermediate wording containing the bare marker
paredit-feature-made the scanner emit an empty-string dependency name and fail differently.declared_feature_dependencies(tests/cli/feature_dependency_contract.rs:53) cannot tolerate the marker appearing without a name after it. Pre-existing; nothing in the tree triggered it before.Reader limitations recorded, not worked around silently
In the package README, with measured counts: Hy's interpolated f-strings unimplemented (390 of 2825 files fail to parse),
#!shebangs not stripped (393 files, exit 0 with junk atoms), Hy bracket strings#[[…]]parsed as code (33 files — the package defends against this explicitly, since a rule could otherwise fire on text that isn't code, and a test pins it), Hy's~not aReaderPrefixsoQuoteState.quasinever counts down for Hy, and LFE#B(…)/#M(…)orphaned from their list in 243 files, inflating arity at exit 0. Plus two more found in passing: LFE|quoted atoms|split at whitespace (95 files) and#\(/#\)restructuring the tree.Same class as the Janet backtick defect fixed in #91, with the same disposition — a reader repair belongs in
core/syntax.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 four fire through the real binary, parsing the JSON
findingsarray, and the pedantic gate is confirmed in both directions:lfe-catch-swallows-exitfires under--preset alland is correctly absent underrecommended. Zero findings sweepingtests/fixtures/**, where the two relevant fixtures are real negative controls rather than empty —corpus/hy.hyhas an immutable default and a typedexcept, both correctly unreported.