From 772d59fff1a9cffa35e7520e3a24016daeba9284 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Fri, 11 Jul 2025 18:22:16 -0500 Subject: [PATCH] more changes to the linting system --- changelog.md | 5 + docs/lints.md | 6 +- src/fennel-ls/config.fnl | 2 +- src/fennel-ls/lint.fnl | 374 +++++++++++++++++---------------------- test/lint.fnl | 53 ++++-- 5 files changed, 217 insertions(+), 223 deletions(-) diff --git a/changelog.md b/changelog.md index 889afcd..131d8e9 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,11 @@ ### Changes * Updated to dkjson 2.8 +### Bug Fixes +* Completions no longer trigger unexpectedly in comments or strings +* When using --lint, diagnostics that don't have source info use ? as a line number instead of line 1 +* lots of code simplification + ## 0.2.1 / 2025-06-06 ### Bug Fixes diff --git a/docs/lints.md b/docs/lints.md index cbfa9a2..b7ef51c 100644 --- a/docs/lints.md +++ b/docs/lints.md @@ -118,12 +118,12 @@ Instead, use: (set alien.health 1337) ``` -# unnecessary-do-values +# unnecessary-unary ## What it does Warns about unnecessary `do` or `values` forms that only contain a single expression. ## Why is this bad? -Extra `do` or `values` forms without multiple expressions add syntactic noise. +Extra forms that don't do anything add syntactic noise. ## Example ```fnl @@ -277,7 +277,7 @@ Instead, use: _ "other") ``` -# multival-in-middle-of-call +# inline-values ## What it does Warns when multiple values from `values` or `unpack` are used in a non-final position of a function call, where only the first value will be used. diff --git a/src/fennel-ls/config.fnl b/src/fennel-ls/config.fnl index c71631c..c36062a 100644 --- a/src/fennel-ls/config.fnl +++ b/src/fennel-ls/config.fnl @@ -26,7 +26,7 @@ There are no global settings. They're all stored in the `server` object. "src/?/init.fnl"] ";")) :lua-version (option "lua54") :lints (collect [_ lint (ipairs lint.list)] - lint.name (option lint.enabled)) + lint.name (option (not lint.disabled))) :libraries (option {}) :extra-globals (option "")}) diff --git a/src/fennel-ls/lint.fnl b/src/fennel-ls/lint.fnl index 54b5fe4..194de7c 100644 --- a/src/fennel-ls/lint.fnl +++ b/src/fennel-ls/lint.fnl @@ -10,30 +10,23 @@ the `file.diagnostics` field, filling it with diagnostics." (local utils (require :fennel-ls.utils)) (local dkjson (require :dkjson)) -(local diagnostic-mt {:__tojson (fn [{: self} state] (dkjson.encode self state)) - :__index #(. $1 :self $2)}) - -(fn diagnostic [self] - (let [fix self.fix] - (set self.fix nil) - (setmetatable {: self : fix} diagnostic-mt))) - (local lints {:definition [] :reference [] :macro-call [] :function-call [] - :special-call [] - :file []}) + :special-call []}) (local all-lints []) -(fn add-lint [code lint] - (set lint.name code) +(fn add-lint [code lint ...] (table.insert all-lints lint) - (if (= (type lint.type) :table) - (each [_ t (ipairs lint.type)] - (table.insert (. lints t) lint)) - (table.insert (. lints lint.type) lint))) + (for [i 1 (select :# lint ...)] + (let [lint (select i lint ...)] + (set lint.name code) + (if (= (type lint.type) :table) + (each [_ t (ipairs lint.type)] + (table.insert (assert (. lints t) (.. "unknown lint type " t)) lint)) + (table.insert (assert (. lints lint.type) (.. "unknown lint type " lint.type)) lint))))) (fn could-be-rewritten-as-sym? [str] (and (= :string (type str)) (not (str:find "^%d")) @@ -42,8 +35,7 @@ the `file.diagnostics` field, filling it with diagnostics." (add-lint :unused-definition {:type :definition - :enabled true - :impl (λ [server file symbol definition] + :impl (fn [server file symbol definition] "local variable that is defined but not used" (if (not (or (= "_" (: (tostring symbol) :sub 1 1)) (= "_" (: (tostring symbol) :sub -1 -1)) @@ -52,14 +44,12 @@ the `file.diagnostics` field, filling it with diagnostics." &until reference] (or (= ref.ref-type :read) (= ref.ref-type :mutate))))) - (diagnostic - {:range (message.ast->range server file symbol) - :message (.. "unused definition: " (tostring symbol)) - :severity message.severity.WARN - :code :unused-definition - :fix #{:title (.. "Replace " (tostring symbol) " with _" (tostring symbol)) - :changes [{:range (message.ast->range server file symbol) - :newText (.. "_" (tostring symbol))}]}})))}) + {:range (message.ast->range server file symbol) + :message (.. "unused definition: " (tostring symbol)) + :severity message.severity.WARN + :fix #{:title (.. "Replace " (tostring symbol) " with _" (tostring symbol)) + :changes [{:range (message.ast->range server file symbol) + :newText (.. "_" (tostring symbol))}]}}))}) ;; this is way too specific; it's also safe to do this inside an `if` or `case` (fn in-or? [calls symbol] @@ -72,200 +62,168 @@ the `file.diagnostics` field, filling it with diagnostics." (let [opts {} item (analyzer.search server file ?ast opts {:stack ?stack})] (if (and (not item) - (. file.lexical symbol) (not (in-or? file.calls symbol)) ;; this doesn't necessarily have to come thru require; it works ;; for built-in modules too opts.searched-through-require-with-stack-size-1) - (diagnostic - {:range (message.ast->range server file symbol) - :message (.. "unknown field: " (tostring symbol)) - :severity message.severity.WARN - :code :unknown-module-field})))) + {:range (message.ast->range server file symbol) + :message (.. "unknown field: " (tostring symbol)) + :severity message.severity.WARN}))) (add-lint :unknown-module-field - {:type :file - :enabled true - :impl (λ [server file] - "any multisym whose definition can't be found through a (require) call" - (icollect [symbol (pairs file.references) &into file.diagnostics] - (if (. (utils.multi-sym-split symbol) 2) - (module-field-helper server file symbol symbol))) - - (icollect [symbol binding (pairs file.definitions) &into file.diagnostics] - (if binding.keys - (module-field-helper server file symbol binding.definition - (fcollect [i (length binding.keys) 1 -1] - (. binding.keys i))))))}) + {:type :reference + :impl (fn [server file symbol] + (if (. (utils.multi-sym-split symbol) 2) + (module-field-helper server file symbol symbol)))} + {:type :definition + :impl (fn [server file symbol definition] + (if definition.keys + (module-field-helper server file symbol definition.definition + (fcollect [i (length definition.keys) 1 -1] + (. definition.keys i)))))}) (add-lint :unnecessary-method {:type :special-call - :enabled true - :impl (λ [server file colon call] + :impl (fn [server file ast] "a call to the : builtin that could just be a multisym" - (if (and (sym? colon ":") - (sym? (. call 2)) - (. file.lexical call)) - (let [method (. call 3)] - (if (could-be-rewritten-as-sym? method) - {:range (message.ast->range server file call) - :message (.. "unnecessary : call: use (" (tostring (. call 2)) - ":" method ")") - :severity message.severity.WARN - :code :unnecessary-method}))))}) + (let [object (. ast 2) + method (. ast 3)] + (if (and (sym? (. ast 1) ":") + (sym? object) + (could-be-rewritten-as-sym? method)) + {:range (message.ast->range server file ast) + :message (.. "unnecessary : call: use (" (tostring object) ":" method ")") + :severity message.severity.WARN})))}) (add-lint :unnecessary-tset {:type :special-call - :enabled true - :impl (λ [server file head call] - (λ all-syms? [call start end] - (faccumulate [syms true - i start end] - (and syms - (could-be-rewritten-as-sym? (. call i))))) + :impl (fn [server file ast] + (let [all-rewritable? (faccumulate [syms true + i 3 (- (length ast) 1) + &until (not syms)] + (could-be-rewritten-as-sym? (. ast i)))] + (if (and (sym? (. ast 1) "tset") + (sym? (. ast 2)) + all-rewritable?) + {:range (message.ast->range server file ast) + :message "unnecessary tset" + :severity message.severity.WARN + :fix #{:title "Replace tset with set" + :changes [{:range (message.ast->range server file ast) + :newText (string.format "(set %s.%s %s)" + (tostring (. ast 2)) + (table.concat ast "." 3 (- (length ast) 1)) + (view (. ast (length ast))))}]}})))}) - (λ make-new-text [call] - (.. (faccumulate [text "(set " - i 2 (- (length call) 2)] - (.. text (tostring (. call i)) ".")) - (tostring (. call (- (length call) 1))) - " " - (view (. call (length call))) - ")")) +(local redundant-wrappers + {:do true :values true :+ true :* true :and true :or true :band true :bor true ".." true}) - (if (and (sym? head :tset) - (sym? (. call 2)) - (all-syms? call 3 (- (length call) 1)) - (. file.lexical call)) - (diagnostic {:range (message.ast->range server file call) - :message (.. "unnecessary " (tostring head)) - :severity message.severity.WARN - :code :unnecessary-tset - :fix #{:title "Replace tset with set" - :changes [{:range (message.ast->range server file call) - :newText (make-new-text call)}]}})))}) - -(add-lint :unnecessary-do-values +(add-lint :unnecessary-unary {:type :special-call - :enabled true - :impl (λ [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 (.. "unnecessary " (tostring head)) - :severity message.severity.WARN - :code :unnecessary-do-values - :fix #{:title "Unwrap the expression" - :changes [{:range (message.ast->range server file call) - :newText (view (. call 2))}]}})))}) + :impl (fn [server file ast] + (if (and (sym? (. ast 1)) + (. redundant-wrappers (tostring (. ast 1))) + (= (length ast) 2)) + {:range (message.ast->range server file ast) + :message (.. "unnecessary " (tostring (. ast 1))) + :severity message.severity.WARN + :fix #{:title "Unwrap the expression" + :changes [{:range (message.ast->range server file ast) + :newText (view (. ast 2))}]}}))}) (local implicit-do-forms (collect [form {: body-form?} (pairs (fennel.syntax))] (values form body-form?))) (add-lint :redundant-do {:type :special-call - :enabled true - :impl (λ [server file head call] - (let [last-body (. call (length call))] - (if (and (. implicit-do-forms (tostring head)) - (. file.lexical call) + :impl (fn [server file ast] + (let [last-body (. ast (length ast))] + (if (and (. implicit-do-forms (tostring (. ast 1))) (list? last-body) - (sym? (. last-body 1) :do) - (not (and (sym? head :do) (= 3 (length call))))) ;; we don't want two lints to trigger for same call - (diagnostic {:range (message.ast->range server file last-body) - :message "redundant do" - :severity message.severity.WARN - :code :redundant-do - :fix #{:title "Unwrap the expression" - :changes [{:range (message.ast->range server file last-body) - :newText (table.concat - (fcollect [i 2 (length last-body)] - (view (. last-body i))) - " ")}]}}))))}) + (sym? (. last-body 1) :do)) + {:range (message.ast->range server file last-body) + :message "redundant do" + :severity message.severity.WARN + :fix #{:title "Unwrap the expression" + :changes [{:range (message.ast->range server file last-body) + :newText (table.concat + (fcollect [i 2 (length last-body)] + (view (. last-body i))) + " ")}]}})))}) (add-lint :bad-unpack {:type :special-call - :enabled true - :impl (λ [server file op call] + :impl (fn [server file call] "an unpack call leading into an operator" - (let [last-item (. call (length call))] + (let [op (. call 1) + last (. call (length call))] (if (and (op? op) ;; last item is an unpack call - (list? last-item) - (or (sym? (. last-item 1) :unpack) - (sym? (. last-item 1) :_G.unpack) - (sym? (. last-item 1) :table.unpack)) - (. file.lexical last-item) - (. file.lexical call)) - (diagnostic - {:range (message.ast->range server file last-item) + (list? last) + (or (sym? (. last 1) :unpack) + (sym? (. last 1) :_G.unpack) + (sym? (. last 1) :table.unpack))) + {:range (message.ast->range server file last) :message (.. "faulty unpack call: " (tostring op) " isn't variadic at runtime." (if (sym? op "..") - (let [unpackme (view (. last-item 2))] + (let [unpackme (view (. last 2))] (.. " Use (table.concat " unpackme ") instead of (.. (unpack " unpackme "))")) (.. " Use a loop when you have a dynamic number of " "arguments to (" (tostring op) ")"))) :severity message.severity.WARN - :code :bad-unpack - :fix (if (and (= (length last-item) 2) + :fix (if (and (= (length last) 2) (sym? op "..")) #{:title "Replace with a call to table.concat" - :changes [{:range (message.ast->range server file (if (= 2 (length call)) call last-item)) - :newText (.. "(table.concat " (view (. last-item 2)) ")")}]})}))))}) + :changes [{:range (message.ast->range server file (if (= 2 (length call)) call last)) + :newText (.. "(table.concat " (view (. last 2)) ")")}]})})))}) -(add-lint :var-not-set +(add-lint :var-never-set {:type :definition - :enabled true - :impl (λ [server file symbol definition] - (if (and definition.var? (not definition.var-set) (. file.lexical symbol)) + :impl (fn [server file symbol definition] + (if (and definition.var? (not definition.var-set)) ;; we can't provide a quickfix for this because the hooks don't give us ;; the full AST of the call to var; just the LHS/RHS - (diagnostic {:range (message.ast->range server file symbol) - :message (.. "var is never set: " (tostring symbol) - " Consider using (local) instead of (var)") - :severity message.severity.WARN - :code :var-never-set})))}) + {:range (message.ast->range server file symbol) + :message (.. "var is never set: " (tostring symbol) + " Consider using (local) instead of (var)") + :severity message.severity.WARN}))}) (local op-identity-value {:+ 0 :* 1 :and true :or false :band -1 :bor 0 :.. ""}) (add-lint :op-with-no-arguments {:type :special-call - :enabled true - :impl (λ [server file op call] + :impl (fn [server file ast] "A call like (+) that could be replaced with a literal" - (let [identity (. op-identity-value (tostring op))] + (let [op (. ast 1) + identity (. op-identity-value (tostring op))] (if (and (op? op) - (= 1 (length call)) - (. file.lexical call) + (= 1 (length ast)) (not= nil identity)) - (diagnostic - {:range (message.ast->range server file call) + {:range (message.ast->range server file ast) :message (.. "write " (view identity) " instead of (" (tostring op) ")") :severity message.severity.WARN - :code :op-with-no-arguments :fix #{:title (.. "Replace (" (tostring op) ") with " (view identity)) - :changes [{:range (message.ast->range server file call) - :newText (view identity)}]}}))))}) + :changes [{:range (message.ast->range server file ast) + :newText (view identity)}]}})))}) (add-lint :no-decreasing-comparison {:type :special-call - :enabled false - :impl (λ [server file op call] - (if (or (sym? op :>) (sym? op :>=)) - (diagnostic - {:range (message.ast->range server file call) - :message "Use increasing operator instead of decreasing" - :severity message.severity.WARN - :code :no-decreasing-comparison - :fix #{:title "Reverse the comparison" - :changes [{:range (message.ast->range server file call) - :newText (let [new (if (sym? op :>=) (fennel.sym :<=) (fennel.sym :<)) - reversed (fcollect [i (length call) 2 -1 - &into (list (sym new))] - (. call i))] - (view reversed))}]}})))}) + :disabled true + :impl (fn [server file ast] + (let [op (. ast 1)] + (if (or (sym? op :>) (sym? op :>=)) + {:range (message.ast->range server file ast) + :message "Use increasing operator instead of decreasing" + :severity message.severity.WARN + :fix #{:title "Reverse the comparison" + :changes [{:range (message.ast->range server file ast) + :newText (let [new (if (sym? op :>=) (fennel.sym :<=) (fennel.sym :<)) + reversed (fcollect [i (length ast) 2 -1 + &into (list (sym new))] + (. ast i))] + (view reversed))}]}})))}) (λ match-reference? [ast references] (if (sym? ast) (?. references ast :target) @@ -275,30 +233,27 @@ the `file.diagnostics` field, filling it with diagnostics." (add-lint :match-should-case {:type :macro-call - :enabled true - :impl (λ [server {: references &as file} ast] + :impl (fn [server {: references &as file} ast] (when (and (list? ast) (sym? (. ast 1) :match) (not (faccumulate [ref false i 3 (length ast) 2 &until ref] (match-reference? (. ast i) references)))) - (diagnostic {:range (message.ast->range server file (. ast 1)) - :message "no pinned patterns; use case instead of match" - :severity message.severity.WARN - :code :match-should-case - :fix #{:title "Replace match with case" - :changes [{:range (message.ast->range server file (. ast 1)) - :newText "case"}]}})))}) + {:range (message.ast->range server file (. ast 1)) + :message "no pinned patterns; use case instead of match" + :severity message.severity.WARN + :fix #{:title "Replace match with case" + :changes [{:range (message.ast->range server file (. ast 1)) + :newText "case"}]}}))}) (add-lint :inline-unpack {:type [:function-call :special-call] - :enabled true - :impl (λ [server file fun call] + :impl (fn [server file call] "generally, values and unpack are signs that the user is trying to do something with multiple values. However, multiple values will get \"adjusted\" to one value if they don't come at the end of the call." (faccumulate [f nil index 2 (length call) &until f] (let [arg (. call index)] - (if (and (not (and (special? fun) (not (op? fun)))) + (if (and (or (op? (. call 1)) (not (special? (. call 1)))) (not= index (length call)) (list? arg) (or (sym? (. arg 1) :values) @@ -308,57 +263,58 @@ the `file.diagnostics` field, filling it with diagnostics." {:range (message.ast->range server file arg) :message (.. "bad " (tostring (. arg 1)) " call: only the first value of the multival will be used") - :severity message.severity.WARN - :code :inline-unpack}))))}) + :severity message.severity.WARN}))))}) (add-lint :empty-let {:type :special-call - :enabled true - :impl (λ [server file _ call] + :impl (fn [server file call] (case call (where [let* binding] - (. file.lexical call) (sym? let* :let) (fennel.sequence? binding) (= 0 (length binding))) - (diagnostic {:range (message.ast->range server file binding) - :message "use do instead of let with no bindings" - :severity message.severity.WARN - :code :empty-let - :fix #{:title "Replace (let [] ...) with (do ...)" - :changes [(let [{: start} (message.ast->range server file let*) - {: end} (message.ast->range server file binding)] - {:range {: start : end} - :newText "do"})]}})))}) + {:range (message.ast->range server file binding) + :message "use do instead of let with no bindings" + :severity message.severity.WARN + :fix #{:title "Replace (let [] ...) with (do ...)" + :changes [(let [{: start} (message.ast->range server file let*) + {: end} (message.ast->range server file binding)] + {:range {: start : end} + :newText "do"})]}}))}) + +(local lint-mt {:__tojson (fn [{: self} state] (dkjson.encode self state)) + :__index #(. $1 :self $2)}) + +(fn wrap [self] + ;; hide `fix` field from the client + (let [fix self.fix] + (set self.fix nil) + (setmetatable {: self : fix} lint-mt))) (λ add-lint-diagnostics [server file] - (each [_ lint (ipairs lints.file)] - (when (. server.configuration.lints lint.name) - (lint.impl server file))) + (fn run [lints ...] + (each [_ lint (ipairs lints)] + (when (. server.configuration.lints lint.name) + (case (lint.impl ...) + diagnostic + (table.insert file.diagnostics + (wrap (doto diagnostic + (tset :code lint.name)))))))) + (each [symbol definition (pairs file.definitions)] (when (. file.lexical symbol) - (each [_ lint (ipairs lints.definition)] - (when (. server.configuration.lints lint.name) - (table.insert file.diagnostics (lint.impl server file symbol definition)))))) + (run lints.definition server file symbol definition))) (each [symbol (pairs file.references)] (when (. file.lexical symbol) - (each [_ lint (ipairs lints.reference)] - (when (. server.configuration.lints lint.name) - (table.insert file.diagnostics (lint.impl server file symbol)))))) + (run lints.reference server file symbol))) (each [[head &as ast] (pairs file.calls)] (when (and (. file.lexical ast) (not= nil head)) - (each [_ lint (ipairs (if (special? head) - lints.special-call - lints.function-call))] - (when (and (. server.configuration.lints lint.name) - (or (not lint.target) (sym? head lint.target))) - (table.insert file.diagnostics (lint.impl server file head ast)))))) - (each [[head &as ast] macroexpanded (pairs file.macro-calls)] + (run (if (special? head) lints.special-call lints.function-call) + server file ast))) + (each [ast macroexpanded (pairs file.macro-calls)] (when (. file.lexical ast) - (each [_ lint (ipairs lints.macro-call)] - (when (and (. server.configuration.lints lint.name) - (or (not lint.target) (sym? head lint.target))) - (table.insert file.diagnostics (lint.impl server file ast macroexpanded))))))) + (run lints.macro-call + server file ast macroexpanded)))) {: add-lint-diagnostics :list all-lints} diff --git a/test/lint.fnl b/test/lint.fnl index e67ae8d..3421cb8 100644 --- a/test/lint.fnl +++ b/test/lint.fnl @@ -196,20 +196,26 @@ (assert-ok "(local tbl {}) (tset tbl \"0123.4567\" 1)") nil) -(fn test-unnecessary-do [] +(fn test-unnecessary-unary [] ;; multi-arg do (assert-ok "(do (print :x) 11)") ;; unnecessary do - (check "(do 9)" [{:message "unnecessary do" - :code :unnecessary-do-values - :range {:start {:character 0 :line 0} - :end {:character 6 :line 0}}}]) + (check "(do 9)" + [{:message "unnecessary do" + :code :unnecessary-unary + :range {:start {:character 0 :line 0} + :end {:character 6 :line 0}}}]) ;; unnecessary values (check "(print :hey (values :lol))" - [{:code :unnecessary-do-values + [{:code :unnecessary-unary :message "unnecessary values" :range {:start {:character 12 :line 0} :end {:character 25 :line 0}}}]) + (check "(+ (* 3) (* 4 4))" + [{:message "unnecessary *" + :code :unnecessary-unary + :range {:start {:character 3 :line 0} + :end {:character 8 :line 0}}}]) nil) (fn test-redundant-do [] @@ -251,10 +257,12 @@ :code :match-should-case :range {:start {:character 1 :line 0} :end {:character 6 :line 0}}}]) + ;; shouldn't trigger on quoted forms + (assert-ok "(macro foo [] `(match x x x))") nil) (fn test-op-with-no-arguments [] - (assert-ok "(and 1)") + (assert-ok "(and 1 2)") (assert-ok "(and false 1)") (assert-ok "(and nil 1)") (check "(and)" @@ -271,7 +279,31 @@ :code :empty-let :range {:start {:character 5 :line 0} :end {:character 7 :line 0}}}]) - (assert-ok "(-> [] (let print))")) + (assert-ok "(-> [] (let print))") + nil) + +(fn test-decreasing-comparison [] + (assert-ok "(let [x 5] (< 1 x 4))") + (assert-ok "(let [x 5] (<= 1 x 4))") + (assert-ok "(let [x 5] (> 4 x 1))") + (assert-ok "(let [x 5] (>= 4 x 1))") + (assert-ok {:main.fnl "(let [x 5] (< 1 x 4))" + :flsproject.fnl "{:lints {:no-decreasing-comparison true}}"}) + (assert-ok {:main.fnl "(let [x 5] (<= 1 x 4))" + :flsproject.fnl "{:lints {:no-decreasing-comparison true}}"}) + (check {:main.fnl "(let [x 5] (> 4 x 1))" + :flsproject.fnl "{:lints {:no-decreasing-comparison true}}"} + [{:message "Use increasing operator instead of decreasing" + :code :no-decreasing-comparison + :range {:start {:character 11 :line 0} + :end {:character 20 :line 0}}}]) + (check {:main.fnl "(let [x 5] (>= 4 x 1))" + :flsproject.fnl "{:lints {:no-decreasing-comparison true}}"} + [{:message "Use increasing operator instead of decreasing" + :code :no-decreasing-comparison + :range {:start {:character 11 :line 0} + :end {:character 21 :line 0}}}]) + nil) ;; TODO lints: ;; duplicate keys in kv table @@ -290,11 +322,12 @@ : test-unknown-module-field : test-unnecessary-method : test-unnecessary-tset - : test-unnecessary-do + : test-unnecessary-unary : test-redundant-do : test-unset-var : test-match-should-case : test-unpack-into-op : test-unpack-in-middle : test-op-with-no-arguments - : test-empty-let} + : test-empty-let + : test-decreasing-comparison}