diff --git a/changelog.md b/changelog.md index 6dcf501..2dad956 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/src/fennel-ls/lint.fnl b/src/fennel-ls/lint.fnl index a991ac7..52813fc 100644 --- a/src/fennel-ls/lint.fnl +++ b/src/fennel-ls/lint.fnl @@ -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)}) diff --git a/test/lint.fnl b/test/lint.fnl index 58db33c..5b0d1b2 100644 --- a/test/lint.fnl +++ b/test/lint.fnl @@ -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}