New nested-associative-operator lint

I want to also get like (if a (if b c)) at some point, but this lint
only covers the ones where flattening fixes it always.
This commit is contained in:
XeroOl 2025-07-17 16:25:46 -05:00
parent ea826576e3
commit 0817af9488
3 changed files with 74 additions and 1 deletions

View File

@ -10,6 +10,7 @@
* Disabled by default because it requires following fennel's optional argument naming conventions
* New lint `invalid-flsproject-settings` checks your `flsproject.fnl` file as you edit it
* Renamed lint `unnecessary-do-values` to `unnecessary-unary` and made it apply to many more forms
* New lint `nested-associative-operator` checks for nested operations that could be flattened (eg. `(+ 1 2 (+ 3 4) 5)`)
### Changes
* Updated to dkjson 2.8

View File

@ -782,6 +782,54 @@ You can read more about how to add lints in docs/linting.md"
:severity message.severity.WARN}))))
nil)})
(local associative-ops {:and true :or true :+ true :* true :band true :bor true :.. true})
(add-lint :nested-associative-operator
{:what-it-does
"Identifies forms that could be written in a flattr way, like `(and foo (and bar baz))`."
:why-care?
"Collapsing nested forms reduces unnecessary nesting and makes code more readable and idiomatic."
:example
"```fnl
(and foo bar (and baz buzz) xyz)
(+ a (+ b c) d)
(or x (or y z))
```
Instead, use:
```fnl
;; Flattened forms:
(and foo bar baz buzz xyz)
(+ a b c d)
(or x y z)
```"
:since "0.2.2-dev"
:type :special-call
:impl (fn [server file ast]
(let [op (. ast 1)]
(when (and (sym? op)
(. associative-ops (tostring op)))
(faccumulate [diagnostic nil
i 2 (length ast)
&until diagnostic]
(let [arg (. ast i)
op-str (tostring op)]
(when (and (list? arg) (= op (. arg 1)))
{:range (message.ast->range server file arg)
:message (.. "nested " op-str " can be collapsed")
:severity message.severity.WARN
:fix #(let [new-form (list (. ast 1))]
(for [j 2 (length ast)]
(let [item (. ast j)]
(if (and (list? item)
(sym? (. item 1) op-str))
(fcollect [k 2 (length item) &into new-form]
(. item k))
(table.insert new-form item))))
{:title (.. "Collapse all nested " op-str)
:changes [{:range (message.ast->range server file ast)
:newText (view new-form)}]})}))))))})
(local lint-mt {:__tojson (fn [{: self} state] (dkjson.encode self state))
:__index #(. $1 :self $2)})

View File

@ -347,6 +347,29 @@
;; unused variable, when a function binding is only used in its body, and the function value is discarded
(fn test-nested-associative-operator []
(check "(and foo (and bar baz) xyz)"
[{:message "nested and can be collapsed"
:code :nested-associative-operator}])
(check "(+ a (+ b c) d)"
[{:message "nested + can be collapsed"
:code :nested-associative-operator}])
(check "(or x (or y z))"
[{:message "nested or can be collapsed"
:code :nested-associative-operator}])
(check "(and foo (and bar baz) (and this that))"
[{:message "nested and can be collapsed"
:code :nested-associative-operator}])
(assert-ok "(and true false true)") ; no nesting
(assert-ok "(+ 1 2 3)") ; no nesting
(assert-ok "(* (+ 1 2) 3)") ; different operations
(assert-ok "(and true (or false true))") ; different operators
nil)
{: test-unused
: test-ampersand
: test-unknown-module-field
@ -362,4 +385,5 @@
: test-empty-let
: test-decreasing-comparison
: test-arg-count
: test-duplicate-keys}
: test-duplicate-keys
: test-nested-associative-operator}