diff --git a/CHANGELOG.md b/CHANGELOG.md index 2215278562..5af634a9f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ For a list of breaking changes, check [here](#breaking-changes). ## Unreleased -- ... +- [#2159](https://github.com/clj-kondo/clj-kondo/issues/2159): New linter, `:invalid-fn-name` ## 2023.12.15 diff --git a/doc/linters.md b/doc/linters.md index c1a56a7269..2a750b9bed 100644 --- a/doc/linters.md +++ b/doc/linters.md @@ -47,6 +47,7 @@ configuration. For general configurations options, go [here](config.md). - [Format](#format) - [Def + fn instead of defn](#def--fn-instead-of-defn) - [Inline def](#inline-def) + - [Invalid fn name](#invalid-fn-name) - [Invalid arity](#invalid-arity) - [Conflicting arity](#conflicting-arity) - [Reduce without initial value](#reduce-without-initial-value) @@ -845,6 +846,21 @@ See [issue](https://github.com/clj-kondo/clj-kondo/issues/1920). *Example message:* `inline def`. +### Invalid fn name + +**Keyword:** `:invalid-fn-name`. + +*Description:* warn when a function's name is not valid. If present, it should +be an unquoted symbol. + +*Default level:* `:error`. + +*Example trigger:* `(fn :fn-name [x] (inc x))`. + +*Example message:* `First arg of fn should be a symbol, params vector or arity clause`. + +*Config:* + ### Invalid arity **Keyword:** `:invalid-arity`. diff --git a/src/clj_kondo/impl/analyzer.clj b/src/clj_kondo/impl/analyzer.clj index 81b28db340..470aa2e22b 100644 --- a/src/clj_kondo/impl/analyzer.clj +++ b/src/clj_kondo/impl/analyzer.clj @@ -934,6 +934,21 @@ (defn- let? [x] (one-of x [[clojure.core let] [cljs.core let]])) +(defn- invalid-fn-name? [?name-expr] + (let [valid? (some-fn utils/list-node? ; fn body (usually 1 of many) + utils/vector-node? ; fn args + utils/symbol-token?)] ; fn name + (not (valid? ?name-expr)))) + +(defn- reg-invalid-fn-name! [ctx expr filename] + (findings/reg-finding! + ctx + (node->line + filename + expr + :invalid-fn-name + "First arg of fn should be a symbol, params vector or arity clause"))) + (defn- def-fn? [{:keys [callstack]}] (let [[_ parent extra-parent] callstack] (or (def? parent) @@ -985,6 +1000,10 @@ (when (and (not (linter-disabled? ctx :def-fn)) (def-fn? ctx)) (reg-def-fn! ctx expr filename)) + (when (and (not (linter-disabled? ctx :valid-fn-name)) + ?name-expr + (invalid-fn-name? ?name-expr)) + (reg-invalid-fn-name! ctx expr filename)) (with-meta parsed-bodies (when arities (cond-> {:arity {:fixed-arities fixed-arities diff --git a/src/clj_kondo/impl/config.clj b/src/clj_kondo/impl/config.clj index b4b59f6f79..ada0ded989 100644 --- a/src/clj_kondo/impl/config.clj +++ b/src/clj_kondo/impl/config.clj @@ -26,6 +26,7 @@ :private-call {:level :error} :inline-def {:level :warning} :def-fn {:level :off} + :invalid-fn-name {:level :error} :redundant-do {:level :warning} :redundant-let {:level :warning} :cond-else {:level :warning} diff --git a/src/clj_kondo/impl/utils.clj b/src/clj_kondo/impl/utils.clj index e320891297..5a9656831f 100644 --- a/src/clj_kondo/impl/utils.clj +++ b/src/clj_kondo/impl/utils.clj @@ -38,6 +38,10 @@ (and (instance? clj_kondo.impl.rewrite_clj.node.seq.SeqNode n) (identical? :set (tag n)))) +(defn vector-node? [n] + (and (instance? clj_kondo.impl.rewrite_clj.node.seq.SeqNode n) + (identical? :vector (tag n)))) + ;;; end export (defn print-err! [& strs] diff --git a/test/clj_kondo/main_test.clj b/test/clj_kondo/main_test.clj index 9701baf938..7d8f61e940 100644 --- a/test/clj_kondo/main_test.clj +++ b/test/clj_kondo/main_test.clj @@ -60,6 +60,18 @@ (is (empty? (lint! "(def x (reify Object (toString [_] \"x\")))" "--lang" (name lang) "--config" (pr-str config)))) (is (empty? (lint! "(require '[some.ns :refer [my-reify]]) (def x (my-reify Object (toString [_] \"x\")))" "--lang" (name lang) "--config" (pr-str config))))))) +(deftest invalid-fn-name-test + (assert-submaps + '({:row 1, :col 1, :level :error, :message "First arg of fn should be a symbol, params vector or arity clause"}) + (lint! "(fn \"fn-name\" [x] (inc x))")) + (assert-submaps + '({:row 1, :col 6, :level :error, :message "First arg of fn should be a symbol, params vector or arity clause"}) + (lint! "(map (fn 'symbol ([x] (inc x))) coll)")) + (assert-submaps + '({:row 1, :col 7, :level :error, :message "First arg of fn should be a symbol, params vector or arity clause"}) + (lint! "(-> 7 (fn [x] (inc x)))")) + (is (empty? (lint! "(fn fn-name [x] (inc x))")))) + (deftest redundant-let-test (let [linted (lint! (io/file "corpus" "redundant_let.clj")) row-col-files (map #(select-keys % [:row :col :file])