Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ paredit-feature-lint-elisp-idiom = { path = "packages/feature/lint-elisp-idiom"
paredit-feature-lint-pathname-io = { path = "packages/feature/lint-pathname-io" }
paredit-feature-lint-clojure-idiom = { path = "packages/feature/lint-clojure-idiom" }
paredit-feature-lint-scheme-idiom = { path = "packages/feature/lint-scheme-idiom" }
paredit-feature-lint-fennel-janet-idiom = { path = "packages/feature/lint-fennel-janet-idiom" }
paredit-feature-lint-type-declaration = { path = "packages/feature/lint-type-declaration" }
paredit-feature-emacs-lisp = { path = "packages/feature/emacs-lisp" }
paredit-feature-conditional-conversion = { path = "packages/feature/conditional-conversion" }
paredit-feature-external-check = { path = "packages/feature/external-check" }
Expand Down
4 changes: 2 additions & 2 deletions docs/src/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ adds each finding's full field set as indented lines under its row.

### Choosing and tuning lint rules

With 303 rules, `inspect lint` needs more than an on/off switch per rule. The
With 313 rules, `inspect lint` needs more than an on/off switch per rule. The
flags below are about the rule *set* rather than about any one rule, and all of
them work with `--list-rules` as well as with a scan — so a run can be
inspected before it is made.
Expand All @@ -446,7 +446,7 @@ key for baselines and suppression tooling.

### Rules a project writes for itself

The 303 shipped rules are the ones everybody gets. A rule like "in *this*
The 313 shipped rules are the ones everybody gets. A rule like "in *this*
codebase, `defentity` must always be given a `:table`" is the majority of what
a mature project wants and none of what a linter can ship, so a project writes
those itself, in Lisp, in `.paredit/rules/*.lisp`:
Expand Down
16 changes: 8 additions & 8 deletions docs/src/reference/architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

`paredit-cli` is a Cargo workspace: a thin composition root plus 62 packages
`paredit-cli` is a Cargo workspace: a thin composition root plus 64 packages
under `packages/core/` and `packages/feature/`. Knowing which package owns a
thing is the fastest way to know where a change belongs.

Expand All @@ -17,7 +17,7 @@ core/syntax ──▶ core/semantics ──▶ core/edit ──▶ core/cli
└──▶ core/workspace core/lint-engine ──┘
feature/* (53 packages, mostly independent of each other)
feature/* (55 packages, mostly independent of each other)
paredit-cli (command tree, dispatch, REGISTRY)
Expand Down Expand Up @@ -65,10 +65,10 @@ src/
A contract test walks `src/` and refuses anything else.

The lint `REGISTRY` is the canonical example of what *must* live here. It names
all 303 rules, and every rule depends on the engine; putting the registry in
all 313 rules, and every rule depends on the engine; putting the registry in
either would be a cycle. So the engine takes a `RuleCatalog` as an argument and
never learns which rules exist, the rules never learn the registry does, and
the registry sits in the root reaching twenty-eight feature packages for their
the registry sits in the root reaching thirty feature packages for their
`META` and `RULE`. That is the criterion: **a module that enumerates or
aggregates several features** belongs in neither core nor any one feature.

Expand Down Expand Up @@ -127,7 +127,7 @@ semantic enum (`ReportLimit::{Complete, Limited(NonZeroUsize)}`,
Derive redundant presentation values (booleans, counts) at the serialization
boundary instead of storing them.

## Lint rules: one trait, one registry line, twenty-eight packages
## Lint rules: one trait, one registry line, thirty packages

The lint suite is the clearest example of the split's shape, and the most
frequently extended part of the tree.
Expand All @@ -141,8 +141,8 @@ frequently extended part of the tree.
| `policy` | Dialect scope, rule selection and gate decisions: logic that needs no tree. |
| `engine` | The single pass, which walks the document once and dispatches each node to every rule whose `head_filter` matches. |

298 of the 303 shipped rules live in twenty-seven themed packages, split seven
ways. A twenty-eighth, `feature/lint-custom`, holds no rules at all: it is the
308 of the 313 shipped rules live in twenty-nine themed packages, split seven
ways. A thirtieth, `feature/lint-custom`, holds no rules at all: it is the
pattern language and the second pass that run the rules a *project* writes for
itself.

Expand Down Expand Up @@ -321,7 +321,7 @@ specifies the two cases R7RS 6.4 leaves open — fixnums compare `eq?` by
guarantee and characters have been normatively `eq?` since 9.0.0.10 — so every
finding there would complain about code the language promises will work.

**`REGISTRY` is in neither.** It names all 303 rules, and every rule depends on
**`REGISTRY` is in neither.** It names all 313 rules, and every rule depends on
the engine, so putting it in the engine or in a rule package would be a cycle.
It sits in the root crate, and the engine receives a `RuleCatalog` as an
argument — which is why the engine can be a package at all.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration

With 303 lint rules and 460 commands, passing every knob as a flag
With 313 lint rules and 460 commands, passing every knob as a flag
stopped scaling. `paredit.toml` is the answer: a small, strictly validated file
that sets the defaults a repository wants, so a command line carries only what
is unusual about *this* invocation.
Expand Down
19 changes: 19 additions & 0 deletions packages/feature/lint-fennel-janet-idiom/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "paredit-feature-lint-fennel-janet-idiom"
description = "Lint rules for Fennel and Janet idiom, the two dialects the catalogue had no dedicated rules for"
readme = "README.md"
publish = false
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
paredit-core-syntax = { path = "../../core/syntax" }
paredit-core-lint-engine = { path = "../../core/lint-engine" }

# Mandatory: without it this package silently opts out of the workspace lint
# table, including `unsafe_code = "deny"`, with no error at all.
[lints]
workspace = true
51 changes: 51 additions & 0 deletions packages/feature/lint-fennel-janet-idiom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# paredit-feature-lint-fennel-janet-idiom

Lint rules whose subject is Fennel or Janet specifically.

Both dialects are first-class in the parser, and until this package the
catalogue had seven rules in scope for either of them — six of which model
several dialects at once and none of which encodes a fact about Fennel or Janet
that is not also true elsewhere. Every rule here is keyed on something the
language's own reference, compiler, or shipped linter states.

| rule | dialects | keyed on | primary source |
| --- | --- | --- | --- |
| `var-never-set` | Fennel, Janet | `var`, `var-` | Fennel `src/linter.fnl` `check-unused`, `"declared as var but never set"` |
| `fennel-deprecated-form` | Fennel | `global`, `require-macros`, `pick-args` | `reference.md`, "Deprecated Forms" |
| `fennel-each-over-non-iterator` | Fennel | `each` | `specials.fnl` `SPECIALS.each`, which emits Lua's generic `for … in` |
| `janet-empty-loop-body` | Janet | `loop`, `seq`, `catseq` | `boot.janet` `check-empty-body`, `maclintf :normal "empty loop body"` |
| `janet-mutating-immutable-literal` | Janet | `put`, `array/*`, `buffer/*` | `src/core/value.c` `janet_put`, which panics on a struct |

## Third-party audit

Run over code nobody here wrote: 288 `.fnl` files (`fennel-lang/fennel`,
`Olical/conjure`, `rktjmp/hotpot.nvim`, `udayvir-singh/tangerine.nvim`,
`min-love2d-fennel`) and 241 `.janet` files (`janet-lang/janet`, `spork`,
`jpm`, `circlet`, `andrewchambers/janet-sh`).

| rule | candidates | findings | adjudication |
| --- | --- | --- | --- |
| `var-never-set` (Fennel) | 210 | 8 | all true; 5 more were false positives from a project-local macro expanding to `set`, now suppressed |
| `var-never-set` (Janet) | 464 | 31 | all true; 5 more were the same macro false positive, now suppressed |
| `fennel-deprecated-form` | 29 | 28 | all true; the 29th is a malformed `(global)` the arity guard declines |
| `fennel-each-over-non-iterator` | 194 | 1 | true — and it is `fennel-lang/fennel`'s own assertion that this shape raises |
| `janet-empty-loop-body` | 174 | 0 | 2 findings before the `:iterate` narrowing, both the deliberate drain idiom |
| `janet-mutating-immutable-literal` | 695 | 0 | unproven: a real denominator, no instance in this corpus |

## Known parser limitation for Janet

Janet's long strings are delimited by a run of backticks of any length
(`src/core/parse.c`, `longstring`), and this repository's reader does not
implement them: a backtick is neither a delimiter nor whitespace for
`Dialect::Janet`, so it is absorbed into an atom and the string's contents are
read as code. Over 241 files from `janet-lang/janet`, `spork`, `jpm`, `circlet`
and `janet-sh`, 9 fail to parse outright and 41 more parse into a tree
containing nodes that lie inside a long string's body. Docstrings written with
```` ``` ```` are the dominant cause, and `src/boot/boot.janet` — Janet's own
core library — is one of the nine.

Every rule here is therefore blind on roughly 4% of real Janet files and can be
handed phantom forms on another 17%. The rules' quote guard does not help:
these nodes are not quoted, they are prose. Fixing the reader is out of this
package's scope; the measurement is recorded here so the limitation is not
rediscovered.
Loading