~xerool/fennel-ls

Add unnecessary-tset, unnecessary-do, and redundant-do lints. v1 APPLIED

Phil Hagelberg: 1
 Add unnecessary-tset, unnecessary-do, and redundant-do lints.

 3 files changed, 94 insertions(+), 6 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~xerool/fennel-ls/patches/55282/mbox | git am -3
Learn more about email & git

[PATCH] Add unnecessary-tset, unnecessary-do, and redundant-do lints. Export this patch

"Redundant" means that it's a `do` in a context where there's already
an implicit `do`, as defined by fennel.syntax indicating body-form? is
true. Unnecessary means it only has one argument.
---
 src/fennel-ls/config.fnl |  3 +++
 src/fennel-ls/lint.fnl   | 47 ++++++++++++++++++++++++++++++++++++-
 test/lint.fnl            | 50 ++++++++++++++++++++++++++++++++++++----
 3 files changed, 94 insertions(+), 6 deletions(-)

diff --git a/src/fennel-ls/config.fnl b/src/fennel-ls/config.fnl
index 9ede9e3..64f6df0 100644
--- a/src/fennel-ls/config.fnl
+++ b/src/fennel-ls/config.fnl
@@ -23,6 +23,9 @@ There are no global settings. They're all stored in the `server` object.
   :lints {:unused-definition (option true)
           :unknown-module-field (option true)
           :unnecessary-method (option true)
           :unnecessary-tset (option true)
           :unnecessary-do (option true)
           :redundant-do (option true)
           :match-should-case (option true)
           :bad-unpack (option true)
           :var-never-set (option true)
diff --git a/src/fennel-ls/lint.fnl b/src/fennel-ls/lint.fnl
index f154f15..580cbcf 100644
--- a/src/fennel-ls/lint.fnl
+++ b/src/fennel-ls/lint.fnl
@@ -2,7 +2,7 @@
Provides the function (check server file), which goes through a file and mutates
the `file.diagnostics` field, filling it with diagnostics."

(local {: sym? : list? : table? : view} (require :fennel))
(local {: sym? : list? : table? : view &as fennel} (require :fennel))
(local analyzer (require :fennel-ls.analyzer))
(local message (require :fennel-ls.message))
(local utils (require :fennel-ls.utils))
@@ -81,6 +81,48 @@ the `file.diagnostics` field, filling it with diagnostics."
         :code 303
         :codeDescription "unnecessary-method"}))))

(λ unnecessary-tset [server file head call]
  (if (and (sym? head :tset) (sym? (. call 2))
           (= :string (type (. call 3))) (. file.lexical call))
      (diagnostic {:range (message.ast->range server file call)
                   :message (string.format "unnecessary %s" head)
                   :severity message.severity.WARN
                   :code 309
                   :codeDescription "unnecessary-tset"}
                  #[{:range (message.ast->range server file call)
                     :newText (string.format "(set %s.%s %s)"
                                             (. call 2) (. call 3)
                                             (view (. call 4)))}])))

(λ unnecessary-do-values [server file head call]
  (if (and (or (sym? head :do) (sym? head :values))
           (= nil (. call 3)) (. file.lexical call))
      (diagnostic {:range (message.ast->range server file call)
                   :message (string.format "unnecessary %s" head)
                   :severity message.severity.WARN
                   :code 310
                   :codeDescription "unnecessary-do-values"}
                  #[{:range (message.ast->range server file call)
                     :newText (view (. call 2))}])))

(local implicit-do-forms (collect [form {: body-form?} (pairs (fennel.syntax))]
                           (values form body-form?)))

(λ redundant-do [server file head call]
  (let [last-body (. call (length call))]
    (if (and (. implicit-do-forms (tostring head)) (. file.lexical call)
             (list? last-body) (sym? (. last-body 1) :do))
        (diagnostic {:range (message.ast->range server file last-body)
                     :message "redundant do"
                     :severity message.severity.WARN
                     :code 311
                     :codeDescription "redundant-do"}
                    #[{:range (message.ast->range server file last-body)
                       :newText (table.concat
                                 (fcollect [i 2 (length last-body)]
                                   (view (. last-body i)))
                                 " ")}]))))

(λ bad-unpack [server file op call]
  "an unpack call leading into an operator"
  (let [last-item (. call (length call))]
@@ -185,6 +227,9 @@ the `file.diagnostics` field, filling it with diagnostics."
      (when head
        (if lints.bad-unpack           (table.insert diagnostics (bad-unpack           server file head call)))
        (if lints.unnecessary-method   (table.insert diagnostics (unnecessary-method   server file head call)))
        (if lints.unnecessary-do       (table.insert diagnostics (unnecessary-do-values server file head call)))
        (if lints.unnecessary-tset     (table.insert diagnostics (unnecessary-tset     server file head call)))
        (if lints.redundant-do         (table.insert diagnostics (redundant-do         server file head call)))
        (if lints.op-with-no-arguments (table.insert diagnostics (op-with-no-arguments server file head call)))

        ;; argument lints
diff --git a/test/lint.fnl b/test/lint.fnl
index 333f25e..f627a13 100644
--- a/test/lint.fnl
+++ b/test/lint.fnl
@@ -177,6 +177,47 @@
         [] [{:code 307}])
  nil)

(fn test-unnecessary-tset []
  ;; valid, if you're targeting older Fennels
  (check "(local [tbl key] [{} :k]) (tset tbl key 249)" [] [{}])
  ;; never a good use of tset
  (check "(local tbl {}) (tset tbl :key 9)"
         [{:code 309
           :codeDescription "unnecessary-tset"
           :message "unnecessary tset"
           :range {:start {:character 15 :line 0}
                   :end {:character 32 :line 0}}}] []))

(fn test-unnecessary-do []
  ;; multi-arg do
  (check "(do (print :x) 11)" [] [{}])
  ;; unnecessary do
  (check "(do 9)" [{:message "unnecessary do"
                    :code 310
                    :codeDescription "unnecessary-do-values"
                    :range {:start {:character 0 :line 0}
                            :end {:character 6 :line 0}}}] [])
  ;; unnecessary values
  (check "(print :hey (values :lol))"
         [{:code 310
           :codeDescription "unnecessary-do-values"
           :message "unnecessary values"
           :range {:start {:character 12 :line 0}
                   :end {:character 25 :line 0}}}]
         []))

(fn test-redundant-do []
  ;; good do
  (check "(case 134 x (do (print :x x) 11))" [] [{}])
  ;; unnecessary one
  (set _G.dbg true)
  (check "(let [x 29] (do (print 9) x))"
         [{:code 311
           :codeDescription "redundant-do"
           :message "redundant do"
           :range {:start {:character 12 :line 0}
                   :end {:character 28 :line 0}}}] []))

(fn test-match-should-case []
  ;; OK: most basic pinning
  (check "(let [x 99] (match 99 x :yep!))" [] [{}])
@@ -207,16 +248,12 @@
                   :end {:character 6 :line 0}}}] []))

;; TODO lints:
;; unnecessary (do) in body position
;; duplicate keys in kv table
;; (tset <sym> <str>) --> (set <sym>.<str>)
;; (tset <sym> <any>) --> (set (. <sym> <any>))
;; (tset <sym> <any>) --> (set (. <sym> <any>)) (might be wanted for compat?)
;; {&as x} and [&as x] pattern with no other matches
;; Unused variables / fields (maybe difficult)
;; discarding results to various calls, such as unpack, values, etc
;; unnecessary `do`/`values` with only one inner form
;; `pairs` or `ipairs` call in a (for) binding table
;; mark when unification is happening on a `match` pattern (may be difficult)
;; steal as many lints as possible from cargo
;; unnecessary parens around single multival destructure

@@ -226,6 +263,9 @@
 : test-ampersand
 : test-unknown-module-field
 : test-unnecessary-colon
 : test-unnecessary-tset
 : test-unnecessary-do
 : test-redundant-do
 : test-unset-var
 : test-match-should-case
 : test-unpack-into-op
-- 
2.39.5