Skip to content

Add a command that removes unused destructured keys - #169

Merged
bbatsov merged 1 commit into
mainfrom
remove-unused-keys
Nov 3, 2026
Merged

bbatsov merged 1 commit into
mainfrom
remove-unused-keys

Conversation

@bbatsov

@bbatsov bbatsov commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

clj-kondo already flags the names a function or let destructures from a map and never uses, and this removes them. It's a structural edit that needs no REPL, so it belongs here rather than in CIDER or clj-refactor. A map left with only :as stays a map, since the bare name would change what a seq binds, and names starting with _ are left alone, as clj-kondo does.

clojure-emacs/clojure-mode#708 adds the same command to clojure-mode.

Comment thread clojure-ts-mode.el Outdated
The whitespace between them and the elements left goes too, but the
closing bracket never ends up on the line of a comment."
(let (regions run)
(dolist (child (append (clojure-ts--value-children coll) (list nil)))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runs are built from the value children only, so a #_ form or a comment between two removed names goes with them. (defn f [{:keys [a #_x c d]}] d) comes out as [{:keys [d]}], and a ;; TODO line between a and c disappears the same way. Ending a run at any comment or dis_expr child would keep them.

Comment thread clojure-ts-mode.el Outdated
elems)))
(cond
((null unused) (when elems (setq kept t)))
((length= unused (length elems)) (setq removed `(,key ,vec ,@removed)))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only counts names, so a :keys vector left with nothing but a #_ form or a comment counts as empty: [{:keys [a #_b]}] becomes [_]. The :or count and the 'empty check for the whole map below have the same problem. The "keeps discarded forms and reader conditionals" spec passes only because the reader conditional is still there.

Comment thread clojure-ts-mode.el Outdated
(delete-region beg end)
(insert text))
(pcase-dolist (`(,beg . ,end) vecs)
(indent-region (marker-position beg) (marker-position end))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This starts on the line where the vector opens, which is the let or fn head and wasn't edited. In

(defn f [x]
    (foo x)
    (let [{:keys [a b]} x] b))

the (let line moves from 4 spaces to 2. Reindenting only the lines after that one would leave it alone.

Comment thread clojure-ts-mode.el Outdated
(interactive)
(let* ((node (or (clojure-ts--node-at-point t)
(user-error "No form at point")))
(top-level (clojure-ts--top-level-node node))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top-level metadata sits next to the form in source, not inside it, so with point on the ^ of ^:private (defn f [{:keys [a b]}] b) this gets the meta_lit and reports nothing to remove. Skipping metadata siblings here would fix it.

Comment thread clojure-ts-mode.el Outdated
(goto-char pos))
(user-error "Must be invoked inside a list")))

(defun clojure-ts--value-children (node)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clojure-ts--unwinding-unsupported-p does this same filter inline, and main now has clojure-ts--counted-child-p for the check.

Comment thread clojure-ts-mode.el
children)
(seq-split children 2))))

(defun clojure-ts--core-form-name (node)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clojure-ts--list-node-sym-text plus the namespace check.

clojure-ts-remove-unused-keys (C-c C-r k) removes what the top-level
form at point destructures from maps but never uses: names in :keys,
:strs and :syms, :as names and names bound to keys, along with their :or
defaults. It covers the arglists of fn, defn, defmethod, letfn and
methods, and the bindings of let-like forms. A map with nothing left
becomes _. Comments and #_ forms among the names stay, and a map that
still has one isn't empty.

A name counts as used when any unqualified symbol with that name
appears in its scope. That ignores shadowing, so it can only miss a
removal. Names starting with _ stay, as do positional args and vector
destructuring. A map left with only :as stays a map, as map
destructuring turns a seq into a map and the bare name wouldn't. Quoted
and syntax-quoted forms are left alone, being data or a macro's
template.
@bbatsov
bbatsov force-pushed the remove-unused-keys branch from 38007a7 to 714b21a Compare November 3, 2026 07:28
@bbatsov
bbatsov merged commit 51df988 into main Nov 3, 2026
10 checks passed
@bbatsov
bbatsov deleted the remove-unused-keys branch November 3, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant