diff --git a/CHANGELOG.md b/CHANGELOG.md index a456522982..032edee802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ For a list of breaking changes, check [here](#breaking-changes). ## Unreleased +- [#1923](https://github.com/clj-kondo/clj-kondo/issues/1923): Lint invalid fn name for threaded fn literals - [#2276](https://github.com/clj-kondo/clj-kondo/issues/2276): New Clojure 1.12 array notation (`String*`) may occur outside of metadata - [#2278](https://github.com/clj-kondo/clj-kondo/issues/2278): `bigint` in CLJS is a known symbol in `extend-type` - [#2288](https://github.com/clj-kondo/clj-kondo/issues/2288): fix static method analysis and suppressing `:java-static-field-call` locally diff --git a/src/clj_kondo/impl/analyzer.clj b/src/clj_kondo/impl/analyzer.clj index 18f2e8b7ea..b4c7888ef5 100644 --- a/src/clj_kondo/impl/analyzer.clj +++ b/src/clj_kondo/impl/analyzer.clj @@ -2107,9 +2107,19 @@ :full-fn-name :row :col :expr] :as m}] - (let [not-is-dot (and (not= '. full-fn-name) + + (let [some-anon-fn-literals? (some #(identical? :fn (tag %)) (:children expr)) + not-is-dot (and (not= '. full-fn-name) (not= '.. full-fn-name))] (cond + some-anon-fn-literals? + (recur ctx (update-in m [:expr :children] + (fn [children] + (for [node children] + (cond-> node + (identical? :fn (tag node)) + (-> macroexpand/expand-fn + (vary-meta assoc :clj-kondo.impl/expanded? true))))))) (and not-is-dot (str/ends-with? full-fn-name ".")) (recur ctx @@ -2772,6 +2782,26 @@ (lint-discouraged-tags! ctx tag-expr) (analyze-children ctx children))) +(defn update-literal-fn-ctx [ctx expr] + (cond-> (assoc ctx :arg-types nil :in-fn-literal true) + (:clj-kondo.impl/fn-has-first-arg (meta expr)) + (update :bindings assoc '% {}))) + +(defn analyze-literal-fn! [lang ctx expr] + (when (identical? :edn lang) + (findings/reg-finding! ctx (assoc (meta expr) + :filename (:filename ctx) + :level :error + :type :syntax + :message "#()s are not allowed in EDN"))) + (when (and (:in-fn-literal ctx) + (not (:clj-kondo.impl/generated expr))) + (findings/reg-finding! ctx (assoc (meta expr) + :filename (:filename ctx) + :level :error + :type :syntax + :message "Nested #()s are not allowed")))) + #_(requiring-resolve 'clojure.set/union) (defn analyze-expression** @@ -2782,7 +2812,7 @@ (:quoted ctx)) (meta/lift-meta-content2 (dissoc ctx :arg-types) expr) expr) - t (tag expr) + t (if (:clj-kondo.impl/expanded? (meta expr)) :exp-fn (tag expr)) {:keys [row col]} (meta expr) arg-count (count (rest children))] (utils/handle-ignore ctx expr) @@ -2837,26 +2867,13 @@ (analyze-children (update ctx :callstack #(cons [nil t] %)) children)) - :fn (do - (when (and (:in-fn-literal ctx) - (not (:clj-kondo.impl/generated expr))) - (findings/reg-finding! ctx (assoc (meta expr) - :filename (:filename ctx) - :level :error - :type :syntax - :message "Nested #()s are not allowed"))) - (when (identical? :edn lang) - (findings/reg-finding! ctx (assoc (meta expr) - :filename (:filename ctx) - :level :error - :type :syntax - :message "#()s are not allowed in EDN"))) - (let [expanded-node (macroexpand/expand-fn expr) - m (meta expanded-node) - has-first-arg? (:clj-kondo.impl/fn-has-first-arg m)] - (recur (cond-> (assoc ctx :arg-types nil :in-fn-literal true) - has-first-arg? (update :bindings assoc '% {})) - expanded-node))) + :exp-fn (do (analyze-literal-fn! lang ctx expr) + (recur (update-literal-fn-ctx ctx expr) + (vary-meta expr assoc :clj-kondo.impl/expanded? false))) + :fn (do (analyze-literal-fn! lang ctx expr) + (let [expanded-node (macroexpand/expand-fn expr)] + (recur (update-literal-fn-ctx ctx expanded-node) + expanded-node))) :token (let [edn? (= :edn lang)] (if (or edn? diff --git a/src/clj_kondo/impl/analyzer/usages.clj b/src/clj_kondo/impl/analyzer/usages.clj index 1420a36927..1ee6f297ac 100644 --- a/src/clj_kondo/impl/analyzer/usages.clj +++ b/src/clj_kondo/impl/analyzer/usages.clj @@ -268,7 +268,7 @@ (and (not generated?) core? (not (:clj-kondo.impl/generated (meta parent-call))) - (one-of core-sym [do fn defn defn- + (one-of core-sym [do fn fn* defn defn- let when-let loop binding with-open doseq try when when-not when-first when-some future]))] @@ -295,7 +295,7 @@ (or core? test?) (not (:clj-kondo.impl/generated (meta parent-call))) (if core? - (one-of core-sym [do fn defn defn- + (one-of core-sym [do fn fn* defn defn- let when-let loop binding with-open doseq try when when-not when-first when-some future]) diff --git a/src/clj_kondo/impl/linters.clj b/src/clj_kondo/impl/linters.clj index ab9c447469..c989a10933 100644 --- a/src/clj_kondo/impl/linters.clj +++ b/src/clj_kondo/impl/linters.clj @@ -512,7 +512,7 @@ ;; doseq always return nil (utils/one-of core-sym [doseq]) (< idx (dec (:len call)))) - (utils/one-of core-sym [do fn defn defn- + (utils/one-of core-sym [do fn fn* defn defn- let when-let loop binding with-open doseq try when when-not when-first when-some future])) diff --git a/src/clj_kondo/impl/utils.clj b/src/clj_kondo/impl/utils.clj index 4410c1a930..3d26de8924 100644 --- a/src/clj_kondo/impl/utils.clj +++ b/src/clj_kondo/impl/utils.clj @@ -19,7 +19,8 @@ ;;; export rewrite-clj functions (defn tag [expr] - (node/tag expr)) + (when (satisfies? node/Node expr) + (node/tag expr))) (def map-node seq/map-node) (def vector-node seq/vector-node) diff --git a/test/clj_kondo/main_test.clj b/test/clj_kondo/main_test.clj index cbd4023201..dce71c247e 100644 --- a/test/clj_kondo/main_test.clj +++ b/test/clj_kondo/main_test.clj @@ -3480,7 +3480,11 @@ foo/"))) '({:file "", :row 1, :col 7, :level :error, :message "Function name must be simple symbol but got: :foo"} {:file "", :row 1, :col 20, :level :error, :message "Function name must be simple symbol but got: :foo"} {:file "", :row 1, :col 33, :level :error, :message "Function name must be simple symbol but got: \"foo\""}) - (lint! "(defn :foo []) (fn :foo []) (fn \"foo\" [])"))) + (lint! "(defn :foo []) (fn :foo []) (fn \"foo\" [])")) + (assert-submaps + '({:file "", :row 1, :col 5, :level :error, :message "Function name must be simple symbol but got: :foo"} + {:file "", :row 1, :col 24, :level :error, :message "Function name must be simple symbol but got: \"foo\""}) + (lint! "(-> :foo #(inc %)) (-> \"foo\" #(inc %))"))) (deftest lint-stdin-exclude-files-test (is (empty? diff --git a/test/clj_kondo/unused_value_test.clj b/test/clj_kondo/unused_value_test.clj index 148fd5d593..f1c924aedb 100644 --- a/test/clj_kondo/unused_value_test.clj +++ b/test/clj_kondo/unused_value_test.clj @@ -146,3 +146,12 @@ '{:linters {:unused-value {:level :warning}} :config-in-call {hyperfiddle.rcf/tests {:linters {:unresolved-symbol {:level :off} :unused-value {:level :off}}}}}))) + +(deftest thread-last-test + (assert-submaps + '({:file "", :row 1, :col 12, :level :warning, :message "Unused value"}) + (lint! "(->> :foo #(name %))" + {:linters {:unused-value {:level :warning} + :redundant-fn-wrapper {:level :off}}})) + (is (empty? (lint! "(->> :foo (#(name %)))" {:linters {:unused-value {:level :warning} + :redundant-fn-wrapper {:level :off}}}))))