diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5bb71..a6bc742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## master (unreleased) +* `analyzer`: include a `:compile-like` key which indicates if the error happened at a "compile-like" phase. + * It represents exceptions that happen at runtime (and therefore never include a `:phase`) which however, represent code that cannot possibly work, and therefore are a "compile-like" exception (i.e. a linter could have caught them). + * The set of conditions which are considered a 'compile-like' exception is private and subject to change. +* Use Orchard [0.15.1](https://github.com/clojure-emacs/orchard/blob/v0.15.1/CHANGELOG.md#0151-2023-09-21). + ## 0.2.0 (2023-08-20) ## Changes diff --git a/project.clj b/project.clj index 66de8e6..0064663 100644 --- a/project.clj +++ b/project.clj @@ -6,7 +6,7 @@ :url "https://github.com/clojure-emacs/haystack" :license {:name "Eclipse Public License" :url "https://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[cider/orchard "0.11.0"] + :dependencies [[cider/orchard "0.15.1"] [instaparse "1.4.12" :exclusions [org.clojure/clojure]]] :pedantic? ~(if (System/getenv "CI") :abort diff --git a/src/haystack/analyzer.clj b/src/haystack/analyzer.clj index 6fc5933..b314da5 100644 --- a/src/haystack/analyzer.clj +++ b/src/haystack/analyzer.clj @@ -338,14 +338,29 @@ (flag-duplicates) (flag-tooling))))) +(defn- compile-like-exception? + "'Compile-like' exceptions are those that happen at runtime + (and therefore never include a `:phase`) which however, + represent code that cannot possibly work, + and therefore are a 'compile-like' exception (i.e. a linter could have caught them)." + [{cause-type :type + ^String + cause-message :message}] + (and (= cause-type 'java.lang.IllegalArgumentException) + (or (some-> cause-message (.startsWith "No matching field")) + (some-> cause-message (.startsWith "No matching method"))))) + (defn- analyze-cause "Analyze the `cause-data` of an exception, in `Throwable->map` format." [cause-data print-fn] (let [pprint-str #(let [writer (StringWriter.)] (print-fn % writer) (str writer)) + phase (-> cause-data :data :clojure.error/phase) m {:class (name (:type cause-data)) - :phase (-> cause-data :data :clojure.error/phase) + :phase phase + :compile-like (boolean (and (not phase) + (compile-like-exception? cause-data))) :message (:message cause-data) :stacktrace (analyze-stacktrace-data (cond (seq (:trace cause-data)) diff --git a/test/haystack/analyzer_test.clj b/test/haystack/analyzer_test.clj index 27472c4..71b9f51 100644 --- a/test/haystack/analyzer_test.clj +++ b/test/haystack/analyzer_test.clj @@ -603,7 +603,52 @@ (eval '(let [1])) (catch Throwable e (sut/analyze e))) - (map :phase)))))))) + (map :phase))))) + (testing "Does not include `:phase` for vanilla runtime exceptions" + (is (= [nil] + (->> (try + (throw (ex-info "" {})) + (catch Throwable e + (sut/analyze e))) + (map :phase))))))) + + (testing "`:compile-like`" + (testing "For non-existing fields" + (is (= [true] + (->> (try + (eval '(.-foo "")) + (catch Throwable e + (sut/analyze e))) + (map :compile-like))))) + (testing "For non-existing methods" + (is (= [true] + (->> (try + (eval '(-> "" (.foo 1 2))) + (catch Throwable e + (sut/analyze e))) + (map :compile-like))))) + (testing "For vanilla exceptions" + (is (= [false] + (->> (try + (throw (ex-info "." {})) + (catch Throwable e + (sut/analyze e))) + (map :compile-like))))) + (testing "For vanilla `IllegalArgumentException`s" + (is (= [false] + (->> (try + (throw (IllegalArgumentException. "foo")) + (catch Throwable e + (sut/analyze e))) + (map :compile-like))))) + (testing "For exceptions with a `:phase`" + (is (#{[false false] ;; normal expectation + [false]} ;; clojure 1.8 + (->> (try + (eval '(let [1])) + (catch Throwable e + (sut/analyze e))) + (map :compile-like))))))) (deftest tooling-frame-name? (are [frame-name expected] (testing frame-name