Skip to content

Commit a983544

Browse files
authored
Merge pull request #160 from clj-commons/hls/20251010-clj-errors
Extend exception parsing to work with `clojure` EDN exception reports
2 parents 1698123 + 9ebfb80 commit a983544

File tree

4 files changed

+192
-34
lines changed

4 files changed

+192
-34
lines changed

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2+
## 3.6.8 -- UNRELEASED
3+
4+
`clj-commons.format.exceptions`:
5+
* `parse-exception` can now parse an exception report generated by the `clojure` or `clj` command.
6+
* New format options :print-length and :print-level (used when pretty-printing exception properties)
7+
8+
[Closed Issues](https://github.com/clj-commons/pretty/milestone/67?closed=1)
9+
110
## 3.6.7 -- 3 Oct 2025
211

312
Adderd a :traditional option when printing or formatting exceptions.

src/clj_commons/format/exceptions.clj

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns clj-commons.format.exceptions
22
"Format and output exceptions in a pretty (structured, formatted) way."
3-
(:require [clojure.pprint :as pp]
3+
(:require [clojure.edn :as edn]
4+
[clojure.pprint :as pp]
45
[clojure.set :as set]
56
[clojure.string :as str]
67
[clj-commons.ansi :refer [compose perr]]
@@ -615,11 +616,11 @@
615616

616617

617618
(defn- format-property-value
618-
[indentation value]
619+
[indentation print-level print-length value]
619620
(let [pretty-value (pp/write value
620621
:stream nil
621-
:length *print-length*
622-
:level *print-level*
622+
:length print-length
623+
:level print-level
623624
:dispatch exception-dispatch)]
624625
(indented-value indentation pretty-value)))
625626

@@ -642,8 +643,10 @@
642643
(defn- render-exception
643644
[exception-stack options]
644645
(let [{show-properties? :properties
645-
:keys [traditional]
646+
:keys [traditional print-level print-length]
646647
:or {show-properties? true
648+
print-level *print-level*
649+
print-length *print-length*
647650
traditional *traditional*}} options
648651
exception-font (:exception *fonts*)
649652
message-font (:message *fonts*)
@@ -672,7 +675,7 @@
672675
:font property-font} k]
673676
": "
674677
[property-font
675-
(format-property-value value-indent (get properties' k))]))
678+
(format-property-value value-indent print-level print-length (get properties' k))]))
676679
sorted-keys)))
677680
"\n"))
678681
exceptions (list
@@ -704,12 +707,14 @@
704707
705708
The options map may have the following keys:
706709
707-
Key | Description
708-
--- |---
709-
:filter | The stack frame filter, which defaults to [[*default-stack-frame-filter*]]
710-
:properties | If true (the default) then properties of exceptions will be output
711-
:frame-limit | If non-nil, the number of stack frames to keep when outputting the stack trace of the deepest exception
712-
:traditional | If true, the use the traditional Java ordering of stack frames.
710+
Key | Description
711+
--- |---
712+
:filter | The stack frame filter, which defaults to [[*default-stack-frame-filter*]]
713+
:properties | If true (the default) then properties of exceptions will be output
714+
:frame-limit | If non-nil, the number of stack frames to keep when outputting the stack trace of the deepest exception
715+
:traditional | If true, the use the traditional Java ordering of stack frames.
716+
:print-level | Override [[*print-level*]]
717+
:print-length | Override [[*print-length*]]
713718
714719
Output may be traditional or modern, as controlled by the :traditonal option
715720
(which defaults to the value of [[*traditional*]]).
@@ -841,26 +846,39 @@
841846
:line-number line-number}
842847
t)))))
843848

844-
(defn parse-exception
845-
"Given a chunk of text from an exception report (as with `.printStackTrace`), attempts to
846-
piece together the same information provided by [[analyze-exception]]. The result
847-
is ready to pass to [[format-exception*]].
848-
849-
This code does not attempt to recreate properties associated with the exceptions; in most
850-
exception's cases, this is not necessarily written to the output. For clojure.lang.ExceptionInfo,
851-
it is hard to distinguish the message text from the printed exception map.
852-
853-
The options are used when processing the stack trace and may include the :filter and :frame-limit keys.
854-
855-
Returns a sequence of exception maps; the final map will include the :stack-trace key (a vector
856-
of stack trace element maps). The exception maps are ordered outermost to innermost (that final map
857-
is the root exception).
858-
859-
This should be considered experimental code; there are many cases where it may not work properly.
860-
861-
It will work quite poorly with exceptions whose message incorporates a nested exception's
862-
.printStackTrace output. This happens too often with JDBC exceptions, for example."
863-
{:added "0.1.21"}
849+
(defn- edn->exception-map
850+
[m]
851+
(let [{:keys [message type data]} m]
852+
(cond-> {:class-name (name type)
853+
:message message}
854+
(seq data) (assoc-in [:properties :data] data))))
855+
856+
(defn- edn->frame
857+
[data]
858+
(let [[class-name method-name source-file line-number] data]
859+
(StackTraceElement. (name class-name)
860+
(name method-name)
861+
source-file
862+
(int line-number))))
863+
864+
(defn- parse-edn-stacktrace
865+
[s options]
866+
(let [data (try
867+
(edn/read-string s)
868+
(catch Throwable _
869+
nil))
870+
root-trace (:clojure.main/trace data)]
871+
(when root-trace
872+
(let [{:keys [via trace]} root-trace
873+
exceptions (mapv edn->exception-map via)
874+
*cache (volatile! {})
875+
stack-trace (->> trace
876+
(map edn->frame)
877+
(map #(transform-stack-trace-element current-dir-prefix *cache %)))
878+
stack-trace' (filter-stack-trace-maps stack-trace options)]
879+
(update exceptions (-> exceptions count dec) assoc :stack-trace stack-trace')))))
880+
881+
(defn- parse-print-stack-trace-output
864882
[exception-text options]
865883
(loop [state :start
866884
lines (str/split-lines exception-text)
@@ -876,15 +894,15 @@
876894
(let [[_ exception-class-name exception-message] (re-matches re-exception-start line)]
877895
(when-not exception-class-name
878896
(throw (ex-info "Unable to parse start of exception."
879-
{:line line
897+
{:line line
880898
:exception-text exception-text})))
881899

882900
;; The exception message may span a couple of lines, so check for that before absorbing
883901
;; more stack trace
884902
(recur :exception-message
885903
more-lines
886904
(conj exceptions {:class-name exception-class-name
887-
:message exception-message})
905+
:message exception-message})
888906
stack-trace
889907
stack-trace-batch))
890908

@@ -923,6 +941,35 @@
923941
(recur :start lines
924942
exceptions stack-trace stack-trace-batch)))))))
925943

944+
(defn parse-exception
945+
"Given a chunk of text from an exception report, attempts to
946+
piece together the same information provided by [[analyze-exception]]. The result
947+
is ready to pass to [[format-exception*]].
948+
949+
An exception report may be in two forms:
950+
951+
* The output from Exception/printStackTrace
952+
* The EDN report generated by the `clojure` or `clj` commands
953+
954+
For printStackTrace output, this does not attempt to recreate properties associated with the exceptions; in most
955+
exception's cases, this is not necessarily written to the output. For clojure.lang.ExceptionInfo,
956+
it is hard to distinguish the message text from the printed exception map.
957+
958+
The options are used when processing the stack trace and may include the :filter and :frame-limit keys.
959+
960+
Returns a sequence of exception maps; the final map will include the :stack-trace key (a vector
961+
of stack trace element maps). The exception maps are ordered outermost to innermost (that final map
962+
is the root exception).
963+
964+
This should be considered experimental code; there are many cases where it may not work properly.
965+
966+
It will work quite poorly with exceptions whose message incorporates a nested exception's
967+
.printStackTrace output. This happens too often with JDBC exceptions, for example."
968+
{:added "0.1.21"}
969+
[exception-text options]
970+
(or (parse-edn-stacktrace exception-text options)
971+
(parse-print-stack-trace-output exception-text options)))
972+
926973
(defn format-stack-trace-element
927974
"Formats a stack trace element into a single string identifying the Java method or Clojure function being executed."
928975
{:added "3.2.0"}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{:clojure.main/message
2+
"Execution error (ExceptionInfo) at expand-bom/ensure-unique-dependencies! (expand_bom.clj:78).\nConflicts in managed dependencies\n",
3+
:clojure.main/triage
4+
{:clojure.error/class clojure.lang.ExceptionInfo,
5+
:clojure.error/line 78,
6+
:clojure.error/cause "Conflicts in managed dependencies",
7+
:clojure.error/symbol expand-bom/ensure-unique-dependencies!,
8+
:clojure.error/source "expand_bom.clj",
9+
:clojure.error/phase :execution},
10+
:clojure.main/trace
11+
{:via
12+
[{:type clojure.lang.Compiler$CompilerException,
13+
:message
14+
"Syntax error macroexpanding at (/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj:1:125).",
15+
:data
16+
{:clojure.error/phase :execution,
17+
:clojure.error/line 1,
18+
:clojure.error/column 125,
19+
:clojure.error/source
20+
"/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj"},
21+
:at [clojure.lang.Compiler load "Compiler.java" 8235]}
22+
{:type clojure.lang.ExceptionInfo,
23+
:message "Conflicts in managed dependencies",
24+
:data
25+
{:conflicts
26+
#{#{{:coordinates [org.apache.httpcomponents/httpclient "4.5.14"],
27+
:source :manually-managed}}}},
28+
:at
29+
[expand_bom$ensure_unique_dependencies_BANG_
30+
invokeStatic
31+
"expand_bom.clj"
32+
78]}],
33+
:trace
34+
[[expand_bom$ensure_unique_dependencies_BANG_
35+
invokeStatic
36+
"expand_bom.clj"
37+
78]
38+
[expand_bom$ensure_unique_dependencies_BANG_
39+
invoke
40+
"expand_bom.clj"
41+
65]
42+
[expand_bom$import_boms invokeStatic "expand_bom.clj" 95]
43+
[expand_bom$import_boms doInvoke "expand_bom.clj" 90]
44+
[clojure.lang.RestFn invoke "RestFn.java" 424]
45+
[clojure.lang.Var invoke "Var.java" 390]
46+
[user$eval2648 invokeStatic "form-init4939043928554804837.clj" 1]
47+
[user$eval2648 invoke "form-init4939043928554804837.clj" 1]
48+
[clojure.lang.Compiler eval "Compiler.java" 7757]
49+
[clojure.lang.Compiler eval "Compiler.java" 7747]
50+
[clojure.lang.Compiler load "Compiler.java" 8223]
51+
[clojure.lang.Compiler loadFile "Compiler.java" 8161]
52+
[clojure.main$load_script invokeStatic "main.clj" 476]
53+
[clojure.main$init_opt invokeStatic "main.clj" 478]
54+
[clojure.main$init_opt invoke "main.clj" 478]
55+
[clojure.main$initialize invokeStatic "main.clj" 509]
56+
[clojure.main$null_opt invokeStatic "main.clj" 543]
57+
[clojure.main$null_opt invoke "main.clj" 540]
58+
[clojure.main$main invokeStatic "main.clj" 665]
59+
[clojure.main$main doInvoke "main.clj" 617]
60+
[clojure.lang.RestFn applyTo "RestFn.java" 140]
61+
[clojure.lang.Var applyTo "Var.java" 707]
62+
[clojure.main main "main.java" 40]],
63+
:cause "Conflicts in managed dependencies",
64+
:data
65+
{:conflicts
66+
#{#{{:coordinates [org.apache.httpcomponents/httpclient "4.5.14"],
67+
:source :manually-managed}}}},
68+
:phase :execution}}
69+

test/clj_commons/exception_test.clj

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,3 +814,36 @@ failed with ABC123"
814814
"clojure.lang.ExceptionInfo: GitHub API request to /repos/nubank/giraffe/pulls/123 returned status 2000"
815815
"clojure.lang.ExceptionInfo: Error handling item"]
816816
(parse-and-format "nested-exception.txt"))))
817+
818+
(deftest clojure-tool-report
819+
(is (match? [" clojure.main.main main.java: 40"
820+
" clojure.lang.Var.applyTo Var.java: 707"
821+
" clojure.lang.RestFn.applyTo RestFn.java: 140"
822+
" clojure.main/main main.clj: 617"
823+
" clojure.main/main main.clj: 665"
824+
" clojure.main/null-opt main.clj: 543"
825+
" clojure.main/initialize main.clj: 509"
826+
" clojure.main/init-opt main.clj: 478"
827+
" clojure.main/load-script main.clj: 476"
828+
" clojure.lang.Compiler.loadFile Compiler.java:8161"
829+
" clojure.lang.Compiler.load Compiler.java:8223"
830+
" clojure.lang.Compiler.eval Compiler.java:7747"
831+
" clojure.lang.Compiler.eval Compiler.java:7757"
832+
" user/eval2648 REPL Input"
833+
" clojure.lang.Var.invoke Var.java: 390"
834+
" clojure.lang.RestFn.invoke RestFn.java: 424"
835+
" expand-bom/import-boms expand_bom.clj: 90"
836+
" expand-bom/import-boms expand_bom.clj: 95"
837+
"expand-bom/ensure-unique-dependencies! expand_bom.clj: 78"
838+
" clojure.lang.ExceptionInfo: Conflicts in managed dependencies"
839+
" data: {:conflicts"
840+
" #{#{{:coordinates [org.apache.httpcomponents/httpclient \"4.5.14\"],"
841+
" :source :manually-managed}}}}"
842+
"clojure.lang.Compiler$CompilerException: Syntax error macroexpanding at (/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj:1:125)."
843+
" data: #:clojure.error{:phase :execution,"
844+
" :line 1,"
845+
" :column 125,"
846+
" :source"
847+
" \"/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj\"}"]
848+
(parse-and-format "sample-clojure-exception.edn" {:print-level 10
849+
:filter (constantly :show)}))))

0 commit comments

Comments
 (0)