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
12 changes: 12 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ paredit-feature-lint-clojure-idiom = { path = "packages/feature/lint-clojure-idi
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-lint-compile-time = { path = "packages/feature/lint-compile-time" }
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
6 changes: 3 additions & 3 deletions docs/src/guide/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ paredit mcp --read-only # …that refuses every command which would write
The server offers a handful of tools — `paredit_check`, `paredit_outline`,
`paredit_lint`, `paredit_format`, `paredit_diff`, `paredit_capabilities` — plus
`paredit_run`, which takes any command's argument vector. **It deliberately does
not expose one tool per command.** There are 460 of them; that many descriptions
not expose one tool per command.** There are 463 of them; that many descriptions
costs thousands of tokens of context before the agent has read a line of code,
and it makes selection harder rather than easier. The catalog is available as
the `paredit://capabilities` resource, and `paredit_run` reaches everything in
Expand Down Expand Up @@ -113,8 +113,8 @@ Each cell carries one of four statuses:

`silent` is the one worth reading carefully. Almost every `inspect` command
exits `0` for every dialect, so a `finding_count` of `0` looks identical
whether the code is clean or the tool has nothing to say about it. 281 of the
460 commands are `silent` for at least one dialect outside Common Lisp, and
whether the code is clean or the tool has nothing to say about it. 284 of the
463 commands are `silent` for at least one dialect outside Common Lisp, and
fourteen of those are `silent` for Common Lisp *itself*, because their subject
is another dialect entirely: `inspect elisp-file` and
`inspect division-result-precision-loss` (Emacs Lisp);
Expand Down
7 changes: 5 additions & 2 deletions docs/src/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ discovery, impact analysis, and preflight checks.
| `scheme-let-star-independent-bindings` | Report a `let*` of two or more bindings whose initializers are all literals or free references, so no binding can see another and the sequential scope buys nothing. Scheme and Racket, **fixable** (only the head symbol is rewritten). |
| `scheme-memq-assq-literal-key` | Report a `memq` or `assq` searching for a number or character literal, which R7RS 6.4 leaves unspecified: `(memq 101 '(100 101 102))` ⟹ *unspecified* while `(memv 101 …)` ⟹ `(101 102)`. Scheme only — Racket specifies both cases, so a finding there would complain about code the language promises will work. **Fixable** (`memq`→`memv`, `assq`→`assv`, which cannot break a working search). |
| `scheme-named-let-never-recurs` | Report a named `let` whose loop name is never mentioned in its body, so it can never iterate and is an ordinary `let` wearing a loop's clothes. Scheme and Racket, **fixable** where the name can simply be deleted. |
| `eval-when-execute-only` | Report a **top-level** `eval-when` naming `:execute` but neither `:compile-toplevel` nor `:load-toplevel`, wrapping a definition: `compile-file` discards the body entirely, so the file loads from source and the compiled fasl is missing the definition. Error severity, Common Lisp only, report-only (which situations were meant is not recoverable from the source). |
| `eval-when-body-never-runs` | Report a **non**-top-level `eval-when` naming only situations the standard ignores there, so its body runs in no phase at all. CLHS 3.2.3.1 makes such a form equivalent to `nil`, and SBCL emits no diagnostic. Error severity, Common Lisp only, report-only (whether `:execute` was meant, or the form wanted hoisting, or deleting, is a judgement). |
| `defconstant-non-eql-value` | Report a `defconstant` whose initform allocates (a string, list, vector or structure literal), so the compile-time and load-time values are not `eql` and a fresh image that compiles and loads the file signals `DEFCONSTANT-UNEQL`. Error severity, Common Lisp only, report-only (`defparameter`, or `define-constant` with which `:test`, is the author's call). |
| `lint` | Run every within-file logic-bug lint at once and report all findings, tagged by rule and category. Each finding is self-describing — it carries its `severity`, `category`, and a `fixable` flag inline (so an agent can triage and decide whether to run `--fix` without cross-referencing `--list-rules`). `--list-rules` prints the rule catalog with categories, descriptions, a `severity` (`error` for likely/certain bugs, `warning` for redundant/non-idiomatic style), and a `fixable` flag marking the rules `--fix` can repair — and it honors the same `--rule`/`--exclude`/`--category` selectors, so `--list-rules --category dead-code` lists just that group; `--rule`/`--exclude` select rules; `--category` selects a whole group (see `--list-rules` for the current set); `--sarif` emits a SARIF 2.1.0 log for CI code scanning (with stable fingerprints and one-click `fixes` for every rule `--list-rules` marks fixable); `--github` emits GitHub Actions `::error::` annotations for inline PR review; `--fix` applies those auto-fixes in place, iterating to a fixpoint (so nested redundancies collapse fully) and reporting the per-file/per-rule counts; add `--diff` to preview the changes as a unified diff without writing, or `--check` to write nothing and exit 3 when any auto-fix is still pending (a CI gate that stays green only when fixable lint has been cleaned up — distinct from `--fail-on-finding`, which also gates on report-only findings). `--check` and `--diff` combine (show the diff and fail). `--fix-plan` instead emits the machine-readable fix plan — each fixable finding's exact byte-region replacements as JSON (or tab-separated text) — without writing, so an editor or agent can preview or apply fixes one at a time (honoring the same suppressions and `--baseline` as `--fix`). Findings can be silenced in source with an inline `; paredit:ignore [rule…]` comment: on its own line it suppresses the next line, trailing after code it suppresses that line, and with no rule names it suppresses every rule — honored uniformly across the report, SARIF, GitHub, and `--fix` outputs. `--fail-on <error\|warning>` gates only on findings at or above a severity (so CI can block on bugs while still reporting style warnings), and SARIF `level` reflects each finding's severity. `--stats` prints a lint-debt rollup instead of individual findings — finding counts by severity, by category, and by rule, plus files-scanned/files-with-findings — honoring the same `--rule`/`--category`/`--baseline` filters. `--suggest-severity` instead prints advisory severity suggestions: for each rule that fired, its findings-per-file density across the scanned workspace (`very high`/`high`/`moderate`/`low`/`very low`), and — only when that disagrees with the rule's current severity — the severity it suggests instead (a currently-`error` rule firing on nearly every file is likely too noisy to gate a build on; a currently-`warning` rule that never fired at all may be rare enough to be worth failing over). This is guidance only: it never writes `paredit.toml`, never changes a rule's declared severity, and never affects this or any later run's exit code. `--report-unused-suppressions` instead reports any `; paredit:ignore` that silences no finding (a stale ignore or a typo'd rule name) and exits 3 if any are found, keeping the ignore list honest in CI. A directive may also carry `-until <date>`; `--report-expired-suppressions` reports any past its date (used or not) and exits 3 if any are found, and `--report-suppressions` lists every directive, used or not, with its scope, rules, reason, and expiry, and always exits 0. `--suppress-path <path>` (repeatable) silences every finding under a path as if it carried `paredit:ignore-file`, for generated/vendored code that cannot hold an inline directive. For adopting the linter on an existing codebase, `--write-baseline <file>` snapshots today's findings and `--baseline <file>` then suppresses those known findings (matched by rule and trimmed-line content, so they survive line shifts) — reporting and gating only on new findings, across the default, `--sarif`, and `--github` outputs. `--fixable` narrows `--list-rules` to just the rules that carry an auto-fix — `paredit fix list` is this pair under a name that says so. |

Most reports accept `--output json` for machine-readable results. Reports whose
Expand All @@ -421,7 +424,7 @@ adds each finding's full field set as indented lines under its row.

### Choosing and tuning lint rules

With 313 rules, `inspect lint` needs more than an on/off switch per rule. The
With 316 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 +449,7 @@ key for baselines and suppression tooling.

### Rules a project writes for itself

The 313 shipped rules are the ones everybody gets. A rule like "in *this*
The 316 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 64 packages
`paredit-cli` is a Cargo workspace: a thin composition root plus 65 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/* (55 packages, mostly independent of each other)
feature/* (56 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 313 rules, and every rule depends on the engine; putting the registry in
all 316 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 thirty feature packages for their
the registry sits in the root reaching thirty-one 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, thirty packages
## Lint rules: one trait, one registry line, thirty-one 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. |

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
311 of the 316 shipped rules live in thirty themed packages, split seven
ways. A thirty-first, `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 313 rules, and every rule depends on
**`REGISTRY` is in neither.** It names all 316 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
4 changes: 2 additions & 2 deletions docs/src/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration

With 313 lint rules and 460 commands, passing every knob as a flag
With 316 lint rules and 463 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 Expand Up @@ -194,7 +194,7 @@ and the dropped keys are named.
| `--from <DIR>` | — | Resolve discovery from this directory instead of the working one |

The flags exist on the `config` namespace; the variables work everywhere,
which is why they exist — 460 commands do not each need three more flags.
which is why they exist — 463 commands do not each need three more flags.

`PAREDIT_NO_CONFIG=1 PAREDIT_NO_CONFIG_ENV=1` is the reproducible-CI
combination: it pins the run to the built-in defaults plus whatever the command
Expand Down
22 changes: 22 additions & 0 deletions packages/feature/lint-compile-time/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "paredit-feature-lint-compile-time"
description = "Lint rules for the Common Lisp compile-time / load-time phase distinction"
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" }
paredit-core-cli = { path = "../../core/cli" }
clap.workspace = true
serde_json.workspace = true

# 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
Loading