From 31e508fa0800766ecfaf8b4a2f4010345b41c09c Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 28 Nov 2024 14:15:02 +0100 Subject: [PATCH 1/2] Fix #2439: uneval applies to next form which may depend on reader features (#2443) Co-authored-by: Noah Bogart --- .../impl/rewrite_clj/parser/core.clj | 39 +++++++++++++++---- parser/clj_kondo/impl/rewrite_clj/reader.clj | 1 + src/clj_kondo/impl/analyzer.clj | 18 +++++---- src/clj_kondo/impl/utils.clj | 27 ++++++++----- test/clj_kondo/impl/parser_test.clj | 8 +++- test/clj_kondo/main_test.clj | 11 +++++- 6 files changed, 76 insertions(+), 28 deletions(-) diff --git a/parser/clj_kondo/impl/rewrite_clj/parser/core.clj b/parser/clj_kondo/impl/rewrite_clj/parser/core.clj index 93e3a93ca4..5a3996318a 100644 --- a/parser/clj_kondo/impl/rewrite_clj/parser/core.clj +++ b/parser/clj_kondo/impl/rewrite_clj/parser/core.clj @@ -6,7 +6,8 @@ [clj-kondo.impl.rewrite-clj.parser.string :refer [parse-string parse-regex]] [clj-kondo.impl.rewrite-clj.parser.token :refer [parse-token]] [clj-kondo.impl.rewrite-clj.parser.utils :as u] - [clj-kondo.impl.rewrite-clj.reader :as reader])) + [clj-kondo.impl.rewrite-clj.reader :as reader] + [clojure.string :as str])) ;; ## Base Parser @@ -166,13 +167,37 @@ {:clj-kondo/ignore (assoc (meta node) :linters v)})))))) +#_(defn spy [x] + (prn x) + x) + (defn- read-with-ignore-hint [reader] - (let [hint (parse-printables reader :uneval 1 true) - im (ignore-meta hint)] - (if im - (vary-meta (parse-next reader) - into im) - (parse-next reader)))) + (let [[node] (parse-printables reader :uneval 1 true) + im (ignore-meta [node]) + ] + (cond im + (vary-meta (parse-next reader) + into im) + (and node + (= :reader-macro (node/tag node)) + (let [sv (-> node :children first :string-value)] + (str/starts-with? sv "?"))) + (let [features reader/*reader-features* + children (when features + (->> node :children last :children + (take-nth 2) + (keep :k) + (into #{})))] + ;; If the reader conditional contains all features or :default, + ;; then it can be ignored. + ;; Otherwise, add :clj-kondo/uneval metadata to discard later. + (if (or (not features) + (every? children features) + (contains? children :default)) + (parse-next reader) + (vary-meta node assoc :clj-kondo/uneval (set (remove children features))))) + :else + (parse-next reader)))) (defmethod parse-next* :sharp [reader] diff --git a/parser/clj_kondo/impl/rewrite_clj/reader.clj b/parser/clj_kondo/impl/rewrite_clj/reader.clj index 5d890621d4..25f9ead075 100644 --- a/parser/clj_kondo/impl/rewrite_clj/reader.clj +++ b/parser/clj_kondo/impl/rewrite_clj/reader.clj @@ -9,6 +9,7 @@ (:import [java.io PushbackReader])) (def ^:dynamic *reader-exceptions* nil) +(def ^:dynamic *reader-features* nil) ;; ## Decisions diff --git a/src/clj_kondo/impl/analyzer.clj b/src/clj_kondo/impl/analyzer.clj index ea022afb5f..cc3acc133d 100644 --- a/src/clj_kondo/impl/analyzer.clj +++ b/src/clj_kondo/impl/analyzer.clj @@ -34,7 +34,7 @@ [clj-kondo.impl.parser :as p] [clj-kondo.impl.rewrite-clj.node.seq :as seq] [clj-kondo.impl.rewrite-clj.node.token :as token] - [clj-kondo.impl.rewrite-clj.reader :refer [*reader-exceptions*]] + [clj-kondo.impl.rewrite-clj.reader :refer [*reader-exceptions* *reader-features*]] [clj-kondo.impl.schema :as schema] [clj-kondo.impl.types :as types] [clj-kondo.impl.utils :as utils :refer @@ -3330,7 +3330,12 @@ ctx (assoc ctx :inline-configs (atom []) :main-ns (atom nil)) - parsed (binding [*reader-exceptions* reader-exceptions] + cljc-config (:cljc config) + features (when (identical? :cljc lang) + (or (:features cljc-config) + [:clj :cljs])) + parsed (binding [*reader-exceptions* reader-exceptions + *reader-features* features] (p/parse-string input)) fname (fs/file-name filename) ctx (case fname @@ -3347,12 +3352,9 @@ (run! #(findings/reg-finding! ctx %) (->findings e filename)))) (case lang :cljc - (let [cljc-config (:cljc config) - features (or (:features cljc-config) - [:clj :cljs])] - (doseq [lang features] - (analyze-expressions (assoc ctx :base-lang :cljc :lang lang :filename filename) - (:children (select-lang ctx parsed lang))))) + (doseq [lang features] + (analyze-expressions (assoc ctx :base-lang :cljc :lang lang :filename filename) + (:children (select-lang ctx parsed lang)))) (:clj :cljs :edn) (let [ctx (assoc ctx :base-lang lang :lang lang :filename filename :uri uri)] diff --git a/src/clj_kondo/impl/utils.clj b/src/clj_kondo/impl/utils.clj index 61af65bb64..b25a36a07f 100644 --- a/src/clj_kondo/impl/utils.clj +++ b/src/clj_kondo/impl/utils.clj @@ -114,16 +114,23 @@ (defn select-lang-children [ctx node lang] (if-let [children (:children node)] - (let [new-children (reduce - (fn [acc node] - (let [splice? (= "?@" (some-> node :children first :string-value))] - (if-let [processed (select-lang ctx node lang splice?)] - (if splice? - (into acc (:children processed)) - (conj acc processed)) - acc))) - [] - children)] + (let [new-children (loop [acc [] + children children] + (if-let [node (first children)] + (let [splice? (= "?@" (some-> node :children first :string-value)) + node-meta (meta node)] + (cond + (not (:clj-kondo/uneval node-meta)) + (if-let [processed (select-lang ctx node lang splice?)] + (if splice? + (recur (into acc (:children processed)) (next children)) + (recur (conj acc processed) (next children))) + (recur acc (next children))) + (contains? (:clj-kondo/uneval node-meta) lang) + (recur acc (drop 2 children)) + :else + (recur acc (next children)))) + acc))] (assoc node :children new-children)) node)) diff --git a/test/clj_kondo/impl/parser_test.clj b/test/clj_kondo/impl/parser_test.clj index 4989f6e9ca..1edbfacc1e 100644 --- a/test/clj_kondo/impl/parser_test.clj +++ b/test/clj_kondo/impl/parser_test.clj @@ -1,11 +1,15 @@ (ns clj-kondo.impl.parser-test (:require [clj-kondo.impl.parser :as parser :refer [parse-string]] - [clj-kondo.impl.rewrite-clj.reader :refer [*reader-exceptions*]] + [clj-kondo.impl.rewrite-clj.reader :refer [*reader-exceptions* + *reader-features*]] [clj-kondo.impl.utils :as utils] [clojure.test :as t :refer [deftest is are]])) (deftest omit-unevals-test - (is (zero? (count (:children (parse-string "#_#_1 2")))))) + (is (zero? (count (:children (parse-string "#_#_1 2"))))) + (is (= 3 (count (:children + (binding [*reader-features* #{:clj :cljs}] + (parse-string "#_#?(:clj 1) 2 3"))))))) (deftest namespaced-maps-test (is (= '#:it{:a 1} (utils/sexpr (utils/parse-string "#::it {:a 1}")))) diff --git a/test/clj_kondo/main_test.clj b/test/clj_kondo/main_test.clj index 229732fa1e..78e653e4eb 100644 --- a/test/clj_kondo/main_test.clj +++ b/test/clj_kondo/main_test.clj @@ -170,7 +170,16 @@ (is (empty? (lint! "^#?(:clj :a :cljsr :b) [1 2 3]" "--lang" "cljc"))) (assert-submaps [{:row 1, :col 1, :level :error, :message "Reader conditionals are only allowed in .cljc files"}] - (lint! "#?(:clj 1)" "--lang" "clj"))) + (lint! "#?(:clj 1)" "--lang" "clj")) + (assert-submaps2 [{:file "", + :row 1, + :col 19, + :level :error, + :message "Expected: number, received: keyword. [clj]"}] + (lint! "(+ 1 #_#?(:clj 2) :three 4)" + {:linters {:type-mismatch {:level :error}} + :output {:langs true}} + "--lang" "cljc"))) (deftest exclude-clojure-test (let [linted (lint! (io/file "corpus" "exclude_clojure.clj"))] From e439bfdb8ca8ed081344f2645f5ae8f84504dcd0 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 28 Nov 2024 14:34:26 +0100 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1da9c7f5a..5ad40f445e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ For a list of breaking changes, check [here](#breaking-changes). - [#2272](https://github.com/clj-kondo/clj-kondo/issues/2272): Report var usage in `if-not` condition as always truthy - [#2433](https://github.com/clj-kondo/clj-kondo/issues/2433): false positive redundant ignore with hook - Document `:cljc` config option. ([@NoahTheDuke](https://github.com/NoahTheDuke)) +- [#2439](https://github.com/clj-kondo/clj-kondo/issues/2439): uneval may apply to nnext form if reader conditional doesn't yield a form ([@NoahTheDuke](https://github.com/NoahTheDuke)) - [#2431](https://github.com/clj-kondo/clj-kondo/issues/2431): only apply redundant-nested-call linter for nested exprs - Relax `:redundant-nested-call` for `comp`, `concat`, `every-pred` and `some-fn` since it may affect performance