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

"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.
This commit is contained in:
Phil Hagelberg 2024-10-02 16:50:59 -07:00 committed by XeroOl
parent a27f42bfa4
commit b5dbeb28fe
3 changed files with 94 additions and 6 deletions

View File

@ -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)

View File

@ -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

View File

@ -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