Repository navigation
Releases: nerima-lisp/paredit-cli
Release list
v1.7.1
New commands
inspect dotfiles: inventory.pareditmetadata without reading file bodies.inspect test-risk: rank named top-level Common Lisp and Emacs Lisp functions without naming-convention test links by complexity. This is static analysis, not execution coverage.refactor bulk: applywrapand syntacticrename-in-formoperations with non-overlapping replacement spans against an original source snapshot, with validated preview and write modes. Rename does not resolve bindings.refactor unwrap-body: remove a wrapper while retaining arguments from--body-startto the end. Multiple forms require a top-level or recognized sequence position. This is structural editing, not a guarantee of preserved semantics.
Fixes
- Extend selected control-flow lint rules to Emacs Lisp
cl-lib, respecting case-sensitive symbol identities. - Support printed Emacs Lisp reader objects as opaque data, preserving their payloads during formatting.
- Add
extract-local-functionguards against assignments to free-variable cells. - Add multi-path
edit killwith overlap rejection, and preflight kill-ring reads. - Validate all selected rewrites before the first write in ordinary lint fixes and refactor steps. Batch rollback is not universally guaranteed.
- Make the package-chain complexity regression control deterministic by counting actual scan work. Existing positive CPU-time bounds remain enforced; the quadratic control must fail both work-growth bounds.
- Measure the Clojure lint cost budget using thread CPU time on Linux and macOS, excluding scheduler waits. Preserve its generated inputs and 6× threshold; this finite-input budget is not proof of linear complexity.
Compatibility
- Rust MSRV remains 1.85.
- Incomplete
inspect dotfilesinventories print their report and exit 3, without an enabling flag. - Capabilities metadata describes unconditional gates and classifies
--intended-bumpas aseveritygate. - Capabilities schema IDs remain unchanged as optional fields are added. Obtain schemas from the same release or binary that produced the document: older cached strict schemas may reject new fields even when
schema_versionmatches.
v1.6.3
A patch release: fixes and text-output changes only. No command, flag, JSON
field, schema_version, exit code, MSRV, or Nix interface changed.
Emacs Lisp formatting
paredit edit format --dialect elisp now agrees with Emacs' own
indent-region in the places it previously did not:
- Special forms and nested special forms align on the body indentation Emacs
produces, rather than on the generic call-argument rule. - Buffer and conditional forms (
with-current-buffer,if,when, and
friends) indent their bodies at the correct depth. - Registered
indentproperties are honoured even when the target width
forces a break. - Escaped
?symbols survive formatting untouched. - A lambda parameter that happens to share a name with a special form stays
inline instead of being indented as one. - Formatting an already-formatted Emacs Lisp file is idempotent.
Expect a diff on the first run over .el files that 1.6.2 formatted.
Blank lines in Emacs Lisp — a changed default
This is the one behaviour change automation should check. With
--max-blank-lines unset, Emacs Lisp now behaves as if --max-blank-lines 1
were given. Unset and 1 are not the same setting: unset discards the source's
blank lines and renders every top-level gap as exactly one, while 1 reads the
source's own count and clamps it.
| Blank lines in source | Unset (1.6.2, and 1.6.3 outside elisp) | 1 (1.6.3 elisp default) |
|---|---|---|
| 0, between top-level forms | 1 — a blank line is inserted | 0 — left adjacent |
| 1 or more, inside a body | 0 — blank lines are discarded | 1 — preserved |
| 2 or more, between top-level forms | 1 | 1 |
;; source
(defun f ()
(progn
1
2))
(defun g () 3)1.6.2 deletes the blank line in f's body and inserts one between f and g;
1.6.3 does neither.
Only the default moved — the mechanism is dialect-independent, and
--max-blank-lines 1 produces exactly this output in every other dialect too.
An explicit --max-blank-lines, or format.blank-lines-max-consecutive in
paredit.toml, still wins.
There is no flag that restores 1.6.2's Emacs Lisp output, because the old
behaviour was "unset" and Emacs Lisp can no longer be unset.
--max-blank-lines 0 is the closest and is still not the same: it discards
blank lines, but it does not insert one between adjacent top-level forms.
paredit edit normalize-quotes
--all now rewrites every form the selector matches. The flag was ignored by
this one command, which refused a multi-match selector as ambiguous — and said
so by suggesting the flag that had just been passed.
Given (list (quote x) (quote y)) and
edit normalize-quotes --query '(quote ?x)' --all:
- 1.6.2 exits 1 with
Error [selection.ambiguous]: --query '(quote ?x)' matches 2 forms; pass --all to act on every match, or narrow the selector. - 1.6.3 exits 0 and prints
(list 'x 'y).
This only turns a previous error into a successful rewrite; no invocation that
succeeded in 1.6.2 changes its output. If a wrapper script catches
selection.ambiguous from this command and retries without --all, that branch
is now unreachable.
paredit edit reindent-defun
A top-level definition that begins at a non-zero column is moved to column zero,
matching what indent-region does to a stray-indented top-level form. This one
is not Emacs Lisp-specific — it applies in every dialect. Spot-check before
running it across a tree that indents some top-level forms deliberately.
paredit inspect capabilities
Exits 0 when the consumer closes stdout early, in every output format:
paredit inspect capabilities --output json | head -5 no longer fails on the
broken pipe. Any || true wrapping this call to swallow that failure can go.
Install
nix run github:nerima-lisp/paredit-cli/v1.6.3 -- --help
nix profile install github:nerima-lisp/paredit-cli/v1.6.3
cargo install --git https://github.com/nerima-lisp/paredit-cli --tag v1.6.3 --lockedv1.6.2
What changed
Nothing in the paredit binary. This is a test-infrastructure release.
paredit-feature-lint-fennel-janet-depth's cost gate asserted a wall-clock ratio
(< 1.8) on rules that cost 16–75 ns per check call in a release build. An
assertion at that magnitude is decided by machine load as much as by the code:
the same tree measures 1.05 in a dev build, 1.27 idle in release, and failed a
downstream Nix package build at 2.04 with nothing touched.
The residual growth is not a complexity change either — doubling the file doubles
the tree, so the traversal's working set stops fitting in cache. That is constant
work per call taking more time per call.
The ratio is now an #[ignore]d benchmark. In its place is an assertion on rule
invocation counts, which the head index and the dialect scope fix before any
check body runs and which are therefore identical on an idle machine and a
loaded one. The O(file)-per-call shape the ratio was chasing is still measured
directly by every_rule_stays_far_cheaper_than_a_materializing_one, which has
four orders of magnitude of headroom against its 10× floor.
This matches the pattern already used in lint-introspection, lint-hy-depth,
lint-form-shape, lint-macro-authoring, lint-documentation and
lint-type-declaration.
Do I have to change anything?
No. No command path, flag, exit code, documented JSON field, Nix output, or MSRV
changed.
One case is worth calling out. If you package paredit-cli yourself and run the
test suite during the build — for example rustPlatform.buildRustPackage, whose
doCheck defaults to true — this release removes the test that could fail your
build under sandbox load. Upstream's own packages.default has always skipped
the suite; it belongs to the nextest flake check and nowhere else.
v1.6.1
Patch release. No command, flag, exit code, or JSON field changed since v1.6.0 — the capabilities catalog (paredit inspect capabilities --output json) diffs identical against the tag.
- Hardened syntax traversal and parallel batch analysis: offset-to-node lookup and child-position lookup are now logarithmic (binary search over ordered children) instead of a linear scan, and batch
fix/analysis scheduling now claims work dynamically across threads instead of static contiguous chunks, so one large file no longer pins a worker while the rest sit idle. Internal only — no observable behavior change. - Documented
paredit editor <file>in the Command model section of the API reference; it was previously only in the README's command table. - Checked in a proptest regression seed for
remove-unusedso that failure stays reproducible in CI.
v1.6.0
New
paredit editor <file>— an interactive, terminal-native text editor for one file: cursor movement, editing actions, and undo/redo, built on the same structural-editing core as the rest of the CLI. (#143)
Fixed
paredit edit format/paredit fixnow align Common Lisp and Emacs Lisp indentation with real Emacs/SLIME conventions: 36 divergence patterns fixed across the operator table, layout algorithms, and reindenter — including argument alignment under a function call's first argument, anif-branch alignment fix, and coverage fordefvar/defstruct/defpackage-family forms. (#144)
Upgrade notes
- Re-running
paredit fix applyorparedit edit format --writeon an already-formatted Common Lisp or Emacs Lisp codebase will reformat call sites whose arguments now align differently — review the diff before committing.
v1.5.0
Highlights
- ~148 new lint rules across Common Lisp, Emacs Lisp, Clojure, Scheme, Racket, Hy, LFE, Carp, Fennel and Janet — condition-system, CLOS-dispatch, data-structure, loop-facility, macro-authoring, declaration, compile-time-phase, and per-dialect idiom rules. Run
paredit inspect capabilities --output jsonto diff the full catalog againstv1.4.0. --fixcorrectness on quoted/reader-prefixed data. Several rules acrosslint-form-shape,lint-numeric,lint-sequence,lint-control-flow, and theleftover-*family could rewrite hard-quoted data literals, delete a reader prefix (#',,@,,.,#+, …), or (forleftover-inspect-call/leftover-print-debug) delete real program output as if it were debug residue. If you ran--fixfor any of these rules underv1.4.0or earlier, re-run underv1.5.0and diff — some of those autofixes were silently corrupting source. See PRs #110, #119–#131, #133–#135, #138–#141.- Reader-correctness fixes across nine dialects. Scheme's
#-dispatch table (#:keyword,#u8(…),##name, R6RS#'/#`/#,/#,@,#nil), Racket's dispatch forms, Carp/Hy/Janet/LFE reader macros and string/comma semantics, Emacs Lisp character literals and radix literals, Common Lisp,.splicing, shebangs, and unused-bracket-as-constituent handling. Files that previously failed to parse (and so received no lint coverage at all) now parse; some previously-accepted files now parse differently — rerunparedit inspect checkon your corpus after upgrading. See PRs #91, #96, #98, #100, #103, #104, #106, #108, #113, #115, #126, #132, #136, #140. equality-aritydemoted fromErrortoWarningafter a false-positive audit (#121, #123, #125).- Nix:
aarch64-darwinadded alongsidex86_64-linux.
Not stable, may still differ
Text output, JSON finding counts (rules were added and sharpened), and edit format layout are explicitly not part of the 1.x compatibility guarantee — see Releases and compatibility.
No breaking changes
Command paths, flags, exit codes, and JSON schema_version are unchanged from v1.4.0.
v1.4.0
Lint
- 21 new CLOS, condition-system, and iteration lint rules (#81).
- 8 new REPL-debug-leftover lint rules catching stray
print/describe/trace-style debugging calls (#78). - Macro hygiene checks are now enforceable (not just report-only) across all eight dialects, with 4 new checks and a scoped auto-fix (#71, #79).
lint-custom(defrule) gained dialect scoping, pattern fragments, self-collision detection, and per-rule timing (#68).defrule's pattern language is now unified with the--query/--rewriteengine — the same pattern syntax works in both places (#62).inspect lint --suggest-severityadvises when a rule's observed finding density disagrees with its configured severity (#57).- Custom-rule parity:
--test-rulesJSON,--docs, and--explainnow cover custom rules the same as built-in ones (#58); loaded custom rules are recognized bylint.enable/disable/deny/warn(#61);config checkdetects custom-rule severity conflicts (K7). - 9 additional lint rules from Section C, part 1 of 13 (#66).
Format
paredit.tomlgained a project-overridable indent table, width profiles, and quote style (#74).- Section J: numeric-case normalization, clause alignment, auto-width, and
.editorconfigsupport (#77). - Trailing-comment alignment and blank-line normalization (#69).
- Fixed width counting, added diff-stat aggregation, and block-comment reindent (#67).
New commands and surfaces
inspect data-checkandedit kill-ring(Phase 0), with a--formatflag and 5 format-specific detectors added todata-check(Phase 1).edit canonicalizenormalizes alist/plist data files, refusing files that carry a comment to avoid silently dropping it (K10, plus a follow-up fix).- A
schemanamespace with adefschemaDSL (Phase 4);config checkrejects duplicate spec keywords and documents:optional. - Named checkpoints for refactor state (FR-008).
--explain-errorand inline ambiguous-selector candidates on refusal (FR-001, FR-002).refactor plan/applygained a risk aggregate, next-command chaining, a compact headline, and grouped apply output (FR-003–007).PAREDIT_CACHE_DIRandcache.dirconfig layering (FR-012).
Semantic analysis
- Type inference widened to Emacs Lisp, Scheme, and Racket (FR-002, FR-003).
- Effects analysis widened to Emacs Lisp (FR-005); contradiction detection widened to Elisp/Scheme/Racket (FR-007).
- Value-layer constant folding/propagation widened to Emacs Lisp and fed into effect purity (FR-004, FR-008/A11).
- Per-dialect
--fail-under-dialectcoverage thresholds (semantic-coverage). - The six semantic introspection commands report per-layer dialect granularity in
inspect capabilities(FR-001). defmethodbinds its parameters, not its specializers, in the semantic model (FR-009).- Read-time-eval handling made cross-dialect (Phase 2).
Agent/MCP experience
- MCP now surfaces write outcome and carries trimmed tool descriptions (FR-009/FR-010).
inspect capabilitiesreports a per-commandwritesflag and each command's possible error codes (FR-011).- Closed a gap where two newer write commands were missing from MCP's
--read-onlygate; fixed anapply/diffconflict.
Accessibility
- Output-wide accessibility:
NO_COLORconsistency, verbosity levels, plain-language errors (#54). TERM=dumb/FORCE_COLORhandling, an HTML palette, screen-reader-friendly structure, JSON envelope pinning, and CJK-aware clipping (#60).
Performance
- Dynamic scheduling, a
Nodestruct size reduction, and a redundant-parse fix for the highest-risk performance/scale items (#76). - Lower-risk performance/scale gap fixes across the engine (#64).
Fixes
config'scustom_conflictsreads now go through the bounded reader.data-checkno longer misclassifiestyped/racket-flavored files asracket-data.
No commands, flags, or JSON fields were removed or renamed since v1.3.0 — this is a purely additive minor release.
v1.3.0
The five shallowest dialects — LFE, Fennel, Janet, Hy, and Carp — gain real
scope analysis, and the dialect capability matrix stops answering "unknown"
for 99% of its cells.
Emacs Lisp gains the same treatment one layer deeper: its own operator
model, a binding table, nine lint rules, and a per-file report.
Added
-
Two error codes.
input.dialect-unsupportedseparates "this file is the
wrong language for this command" frominput.shape-refused, because the two
call for opposite actions — stop retrying here, versus select a different
form in this file.argument.flag-combinationcovers the sixteen places that
each spelled "these flags do not combine" in their own words. -
Three new namespaces —
query,fix,migrate. The existing three
split by what a change costs to undo (inspectreads,edittransforms one
form,refactorplans and applies). These split by what the caller is
trying to do, over a file set rather than one form. Nothing is removed and
no older spelling stops working.paredit querypromotes the--querypattern language out of the selector
position, where it could only name a form in one named file.query find
searches a whole workspace and reports each match with its captures, path,
and stable selector id, through the shared report envelope (so--output sarif,junit,csvand the rest come with it) and with the full input
surface (--since origin/main,--from-git,--include).query count
tallies several patterns side by side over one file set, which is what makes
it a migration's progress bar rather than a number.query replaceis the
one genuinely new capability: a--rewritetemplate whose?name
placeholders are filled with the verbatim source bytes the pattern's
?namecaptured, so a captured1.0d0stays a double float and a captured
string keeps its escapes — nothing is re-serialized, because re-serializing
is how a rewrite turns1.0d0into1.paredit fixis the write side ofinspect lint, under a name that says it
writes.fix apply,fix check,fix planandfix listwere--fix,
--fix --check,--fix-planand--list-rules; each builds the arguments
its old spelling produced and calls the same engine, so the two paths are
identical by construction rather than by testing.inspect lintalso gains
--fixable, which narrows--list-rulesto the rules that carry a fix.paredit migrateruns a recipe: an ordered list of query/rewrite steps
scoped to the dialects the rewrite is correct for. Order matters —
(if (not p) a nil)becomes(unless p a)only if the negated step runs
first — and so does scope:(incf x)→(cl-incf x)modernizes Emacs Lisp
and breaks Common Lisp, whereincfis the correct spelling. Two recipes
ship (elisp-cl-lib,nil-conditionals), as embedded Lisp source parsed by
the same reader a project's own.paredit/migrations/*.lispgoes through.Both writing commands refuse three situations that leave source which still
parses and is still wrong, and which the reparse guard therefore cannot
catch: a match nested inside one already rewritten, a rewrite that would
delete a comment no capture carries over, and a match inside quoted data —
'(a (if x y nil) b)is a list literal, and rewriting it changes the
program's data rather than its code. All three are counted in every output
format, including as zeroes;--allow-comment-lossand--include-quoted
override the two that are overridable.query find/query count/query replaceandmigrate runsurvive a file
that will not parse: it is reported on stderr and excluded, and the files
that did parse still get a report. Only a run where every file fails is a
hard error.migrate runalso skips a step before parsing it when none of
the step's pattern's literal atoms appear in the file's current text — a
sound, one-directional prefilter that avoids materializing a syntax tree
for a step that cannot match. -
Five clone-detection commands built on the existing tree-edit-distance
scorer, whichsimilarityandduplicateshad been under-using.
inspect clone-classesgroups near-duplicate forms into classes, labels each
on the standard Type-1/2/3 taxonomy, and ranks them by the lines extracting
one would save — five copies of a helper are one class to act on rather than
ten pairs to read.inspect clone-sequencesfinds duplicated runs of adjacent
sibling forms, the sub-form clones that no whole-form report can see because
the duplication does not line up with a form boundary.inspect clone-external
compares the project against a reference corpus across head symbols, so a
localjoin-stringsmatches a librarystr:join.inspect clone-threshold
recommends a--thresholdfrom the project's own similarity distribution,
reporting the histogram, an Otsu split, the widest distribution gap and
percentiles so the recommendation can be judged rather than taken.
inspect clone-genealogyorders each class by the commit that introduced its
members, separating the original from the copies. -
inspect similaritylabels every reported pair with its clone type, the
number of atoms renamed, and whether that renaming is a bijection. An
inconsistent Type-2 renaming is the shape a copy-paste bug takes when one
occurrence of a variable was missed. -
Six new ways to choose which files get analysed. Discovery was a
directory walk with four booleans and an exact-path exclude list. It could
not be told to respect.gitignore, to take the file set from a build
definition, or to look only at what a pull request changed. The flags now
divide into selectors, which decide where the candidate list comes from,
and filters, which narrow whatever the selector produced:--since <git-ref>— only the files that differ from a ref, which is the
single biggest lever on CI time. Deleted files drop out, a rename reports
its destination, and an unresolvable ref is an error rather than an empty
change set that would let a gate pass without examining anything.--from-git— the file set fromgit ls-files.--from-manifest— the project's own build definition: ASDFdefsystem
:components(through:moduleand:pathname, in declaration order),
deps.edn,shadow-cljs.edn,project.clj, and an Emacs Lisp
Package-Requiresheader. A named component missing from disk is reported
rather than silently dropped.--paths-from <FILE|->— a list you computed yourself, newline- or
NUL-separated.--from-archive <ARCHIVE|-> --extract-to <DIR>— an uncompressed tar, from
a file or stdin. Compression and transport stay with the shell, which keeps
an HTTP client, a TLS stack and a decompressor out of a tool trusted with
your source tree. Extraction refuses rather than sanitises absolute paths,
..components, symlinks, hardlinks, devices, and overwriting.--cache-dir <DIR>— reuse a previous scan. Keyed on everything that can
change the selection and validated against the tree, so a stale entry is a
miss rather than a wrong answer.
-
Filters:
--include/--exclude-globingitignore(5)syntax,
.gitignoreand.pareditignorehonoured by default (--no-gitignore,
--no-pareditignore,--no-ignore, and threePAREDIT_NO_*environment
variables), and--follow-symlinks. Ignore precedence follows git exactly:
the deeper file wins, the last matching pattern within a file wins, a
repository boundary cuts the stack, and a root named on the command line is
never ignore-filtered. -
paredit inspect sources. Runs selection and stops, reporting which rule
dropped every file that did not make it. It parses nothing, which makes it
the cheapest way to answer "did that CI run find nothing, or did my pattern
find nothing". -
Discovery reports repository boundaries, so a run over several checkouts
groups its result per repository instead of flattening them. -
Six new ways to select a form.
--pathand--atwere the only two,
and both cost a round trip to build: an agent had to runinspect outline,
read a path out of it, and hope nothing moved in between. Every command that
takes a target now also accepts:--query '(defun ?name ...)'— an S-expression pattern, written in the
file's own dialect and read with its own reader._matches one form,
?namebinds one,...matches a run,?body...binds a run. Captures
may be constrained (?x:list,?x:number, …), a repeated name is a
back-reference ((eq ?x ?x)finds self-comparisons), and--capture name
selects the bound sub-form rather than the whole match.--name <symbol>— the definition of that name, at any nesting depth.--line-column LINE[:COLUMN]— a 1-based editor coordinate, columns
counted in characters. The column defaults to 1.--id <id>— a content-addressed id that keeps naming the same form after
edits elsewhere in the file, where a--pathwould not.--from/--to— a contiguous range of siblings, each end given as a
compact selector (0.2,at:120,name:foo,query:(defun ?n ...)).--parent/--child N/--sibling ±N— relative moves over any of the
above, applied up-across-down.--select <selector>— any of the above in one flag, using the compact
grammar. This is how the richer selectors reach the eight commands whose
own flags already claim these names (refactor introduce-let --nameis
the new binding's name,rename-binding --from/--toare symbols):
introduce-let,inline-let,remove-unused-binding,
thread-expression,unthread-expression,unwrap-call,
extract-function,extract-constantnow take--path,--at, and
--select. They also report the resolved path in their JSON plan whe...
v1.2.1
No command, flag, exit code, or JSON field changed in this release: nothing
under src/ or packages/ was touched. What changed is how the project
verifies itself. The whole verification gate went from 27 minutes to 8, by
removing work it was doing twice rather than by checking less.
Changed
- The Rust flake checks are crane
derivations sharing pre-built dependency artifacts, instead of four
independentbuildRustPackagederivations that each recompiled the whole
dependency graph. Consumers ofpackages.defaultoroverlays.defaultget
the same binary; what changed is that building it no longer runs the test
suite, which now belongs tochecks.<system>.nextestalone.flake.nix
gains acraneinput, andlib.<system>.ciCheckNamesis exported so CI can
derive its job matrix from the checks rather than restating them. checks.<system>.msrvstops atcargo check --all-targetsinstead of
building and testing the whole workspace under the MSRV toolchain. It
verifies the same thing — that the declared MSRV still compiles the
workspace — without a third full run of the test suite.- The
devprofile emits line tables instead of full debug information.
Backtraces still resolve every frame tofile:line; use
RUSTFLAGS=-Cdebuginfo=2when step-debugging. - CI runs one job per flake check instead of one
nix flake checkfor all of
them, and derives that job list from the flake so a new check cannot silently
stop being verified.
v1.2.0
No command, flag, exit code, or JSON field changed in this release: the
capability catalogue is byte-identical to v1.1.0. What changed is the shape of
the source tree behind it, and — for anyone using the crate as a library — the
type of every error it can return.
Changed
- The single crate is now a 24-package Cargo workspace: six
paredit-core-*
packages (syntax, semantics, edit, lint-engine, workspace, cli) and eighteen
paredit-feature-*packages, with the binary as a thin composition root.
Theparedit_clifaçade re-exports every package module, so existing import
paths such asparedit_cli::domain::sexprstill resolve. - The lint engine no longer depends on the rule registry. Rules are supplied
through aRuleCatalog, which breaks the cycle that previously forced the
engine and the 134 rules into one compilation unit. - Breaking for library consumers. Fallible entry points return typed error
enums instead ofanyhow::Result—SexprError,EditRefusal,LintError,
CliError, and per-feature equivalents. A caller that names a return type or
matches on an error must be updated;?into ananyhow::Resultstill
compiles unchanged. The CLI's own stderr text is unchanged, becausemain
still converts at the boundary. - Several types now make invalid states unrepresentable rather than checking
for them: inline-function's call selection, split-file's destination, and the
refactor manifest's hash and flag comparisons, which are derived rather than
stored. - The compatibility guide no longer lists the Rust library API as a stable
surface. The crate ispublish = falsewith no registry release, and the
supported interface is the command line — see
Releases and compatibility.