Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@

## 3.6.8 -- UNRELEASED

`clj-commons.format.exceptions`:
* `parse-exception` can now parse an exception report generated by the `clojure` or `clj` command.
* New format options :print-length and :print-level (used when pretty-printing exception properties)

[Closed Issues](https://github.com/clj-commons/pretty/milestone/67?closed=1)

## 3.6.7 -- 3 Oct 2025

Adderd a :traditional option when printing or formatting exceptions.
Expand Down
115 changes: 81 additions & 34 deletions src/clj_commons/format/exceptions.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns clj-commons.format.exceptions
"Format and output exceptions in a pretty (structured, formatted) way."
(:require [clojure.pprint :as pp]
(:require [clojure.edn :as edn]
[clojure.pprint :as pp]
[clojure.set :as set]
[clojure.string :as str]
[clj-commons.ansi :refer [compose perr]]
Expand Down Expand Up @@ -615,11 +616,11 @@


(defn- format-property-value
[indentation value]
[indentation print-level print-length value]
(let [pretty-value (pp/write value
:stream nil
:length *print-length*
:level *print-level*
:length print-length
:level print-level
:dispatch exception-dispatch)]
(indented-value indentation pretty-value)))

Expand All @@ -642,8 +643,10 @@
(defn- render-exception
[exception-stack options]
(let [{show-properties? :properties
:keys [traditional]
:keys [traditional print-level print-length]
:or {show-properties? true
print-level *print-level*
print-length *print-length*
traditional *traditional*}} options
exception-font (:exception *fonts*)
message-font (:message *fonts*)
Expand Down Expand Up @@ -672,7 +675,7 @@
:font property-font} k]
": "
[property-font
(format-property-value value-indent (get properties' k))]))
(format-property-value value-indent print-level print-length (get properties' k))]))
sorted-keys)))
"\n"))
exceptions (list
Expand Down Expand Up @@ -704,12 +707,14 @@

The options map may have the following keys:

Key | Description
--- |---
:filter | The stack frame filter, which defaults to [[*default-stack-frame-filter*]]
:properties | If true (the default) then properties of exceptions will be output
:frame-limit | If non-nil, the number of stack frames to keep when outputting the stack trace of the deepest exception
:traditional | If true, the use the traditional Java ordering of stack frames.
Key | Description
--- |---
:filter | The stack frame filter, which defaults to [[*default-stack-frame-filter*]]
:properties | If true (the default) then properties of exceptions will be output
:frame-limit | If non-nil, the number of stack frames to keep when outputting the stack trace of the deepest exception
:traditional | If true, the use the traditional Java ordering of stack frames.
:print-level | Override [[*print-level*]]
:print-length | Override [[*print-length*]]

Output may be traditional or modern, as controlled by the :traditonal option
(which defaults to the value of [[*traditional*]]).
Expand Down Expand Up @@ -841,26 +846,39 @@
:line-number line-number}
t)))))

(defn parse-exception
"Given a chunk of text from an exception report (as with `.printStackTrace`), attempts to
piece together the same information provided by [[analyze-exception]]. The result
is ready to pass to [[format-exception*]].

This code does not attempt to recreate properties associated with the exceptions; in most
exception's cases, this is not necessarily written to the output. For clojure.lang.ExceptionInfo,
it is hard to distinguish the message text from the printed exception map.

The options are used when processing the stack trace and may include the :filter and :frame-limit keys.

Returns a sequence of exception maps; the final map will include the :stack-trace key (a vector
of stack trace element maps). The exception maps are ordered outermost to innermost (that final map
is the root exception).

This should be considered experimental code; there are many cases where it may not work properly.

It will work quite poorly with exceptions whose message incorporates a nested exception's
.printStackTrace output. This happens too often with JDBC exceptions, for example."
{:added "0.1.21"}
(defn- edn->exception-map
[m]
(let [{:keys [message type data]} m]
(cond-> {:class-name (name type)
:message message}
(seq data) (assoc-in [:properties :data] data))))

(defn- edn->frame
[data]
(let [[class-name method-name source-file line-number] data]
(StackTraceElement. (name class-name)
(name method-name)
source-file
(int line-number))))

(defn- parse-edn-stacktrace
[s options]
(let [data (try
(edn/read-string s)
(catch Throwable _
nil))
root-trace (:clojure.main/trace data)]
(when root-trace
(let [{:keys [via trace]} root-trace
exceptions (mapv edn->exception-map via)
*cache (volatile! {})
stack-trace (->> trace
(map edn->frame)
(map #(transform-stack-trace-element current-dir-prefix *cache %)))
stack-trace' (filter-stack-trace-maps stack-trace options)]
(update exceptions (-> exceptions count dec) assoc :stack-trace stack-trace')))))

(defn- parse-print-stack-trace-output
[exception-text options]
(loop [state :start
lines (str/split-lines exception-text)
Expand All @@ -876,15 +894,15 @@
(let [[_ exception-class-name exception-message] (re-matches re-exception-start line)]
(when-not exception-class-name
(throw (ex-info "Unable to parse start of exception."
{:line line
{:line line
:exception-text exception-text})))

;; The exception message may span a couple of lines, so check for that before absorbing
;; more stack trace
(recur :exception-message
more-lines
(conj exceptions {:class-name exception-class-name
:message exception-message})
:message exception-message})
stack-trace
stack-trace-batch))

Expand Down Expand Up @@ -923,6 +941,35 @@
(recur :start lines
exceptions stack-trace stack-trace-batch)))))))

(defn parse-exception
"Given a chunk of text from an exception report, attempts to
piece together the same information provided by [[analyze-exception]]. The result
is ready to pass to [[format-exception*]].

An exception report may be in two forms:

* The output from Exception/printStackTrace
* The EDN report generated by the `clojure` or `clj` commands

For printStackTrace output, this does not attempt to recreate properties associated with the exceptions; in most
exception's cases, this is not necessarily written to the output. For clojure.lang.ExceptionInfo,
it is hard to distinguish the message text from the printed exception map.

The options are used when processing the stack trace and may include the :filter and :frame-limit keys.

Returns a sequence of exception maps; the final map will include the :stack-trace key (a vector
of stack trace element maps). The exception maps are ordered outermost to innermost (that final map
is the root exception).

This should be considered experimental code; there are many cases where it may not work properly.

It will work quite poorly with exceptions whose message incorporates a nested exception's
.printStackTrace output. This happens too often with JDBC exceptions, for example."
{:added "0.1.21"}
[exception-text options]
(or (parse-edn-stacktrace exception-text options)
(parse-print-stack-trace-output exception-text options)))

(defn format-stack-trace-element
"Formats a stack trace element into a single string identifying the Java method or Clojure function being executed."
{:added "3.2.0"}
Expand Down
69 changes: 69 additions & 0 deletions test-resources/sample-clojure-exception.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{:clojure.main/message
"Execution error (ExceptionInfo) at expand-bom/ensure-unique-dependencies! (expand_bom.clj:78).\nConflicts in managed dependencies\n",
:clojure.main/triage
{:clojure.error/class clojure.lang.ExceptionInfo,
:clojure.error/line 78,
:clojure.error/cause "Conflicts in managed dependencies",
:clojure.error/symbol expand-bom/ensure-unique-dependencies!,
:clojure.error/source "expand_bom.clj",
:clojure.error/phase :execution},
:clojure.main/trace
{:via
[{:type clojure.lang.Compiler$CompilerException,
:message
"Syntax error macroexpanding at (/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj:1:125).",
:data
{:clojure.error/phase :execution,
:clojure.error/line 1,
:clojure.error/column 125,
:clojure.error/source
"/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj"},
:at [clojure.lang.Compiler load "Compiler.java" 8235]}
{:type clojure.lang.ExceptionInfo,
:message "Conflicts in managed dependencies",
:data
{:conflicts
#{#{{:coordinates [org.apache.httpcomponents/httpclient "4.5.14"],
:source :manually-managed}}}},
:at
[expand_bom$ensure_unique_dependencies_BANG_
invokeStatic
"expand_bom.clj"
78]}],
:trace
[[expand_bom$ensure_unique_dependencies_BANG_
invokeStatic
"expand_bom.clj"
78]
[expand_bom$ensure_unique_dependencies_BANG_
invoke
"expand_bom.clj"
65]
[expand_bom$import_boms invokeStatic "expand_bom.clj" 95]
[expand_bom$import_boms doInvoke "expand_bom.clj" 90]
[clojure.lang.RestFn invoke "RestFn.java" 424]
[clojure.lang.Var invoke "Var.java" 390]
[user$eval2648 invokeStatic "form-init4939043928554804837.clj" 1]
[user$eval2648 invoke "form-init4939043928554804837.clj" 1]
[clojure.lang.Compiler eval "Compiler.java" 7757]
[clojure.lang.Compiler eval "Compiler.java" 7747]
[clojure.lang.Compiler load "Compiler.java" 8223]
[clojure.lang.Compiler loadFile "Compiler.java" 8161]
[clojure.main$load_script invokeStatic "main.clj" 476]
[clojure.main$init_opt invokeStatic "main.clj" 478]
[clojure.main$init_opt invoke "main.clj" 478]
[clojure.main$initialize invokeStatic "main.clj" 509]
[clojure.main$null_opt invokeStatic "main.clj" 543]
[clojure.main$null_opt invoke "main.clj" 540]
[clojure.main$main invokeStatic "main.clj" 665]
[clojure.main$main doInvoke "main.clj" 617]
[clojure.lang.RestFn applyTo "RestFn.java" 140]
[clojure.lang.Var applyTo "Var.java" 707]
[clojure.main main "main.java" 40]],
:cause "Conflicts in managed dependencies",
:data
{:conflicts
#{#{{:coordinates [org.apache.httpcomponents/httpclient "4.5.14"],
:source :manually-managed}}}},
:phase :execution}}

33 changes: 33 additions & 0 deletions test/clj_commons/exception_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -814,3 +814,36 @@ failed with ABC123"
"clojure.lang.ExceptionInfo: GitHub API request to /repos/nubank/giraffe/pulls/123 returned status 2000"
"clojure.lang.ExceptionInfo: Error handling item"]
(parse-and-format "nested-exception.txt"))))

(deftest clojure-tool-report
(is (match? [" clojure.main.main main.java: 40"
" clojure.lang.Var.applyTo Var.java: 707"
" clojure.lang.RestFn.applyTo RestFn.java: 140"
" clojure.main/main main.clj: 617"
" clojure.main/main main.clj: 665"
" clojure.main/null-opt main.clj: 543"
" clojure.main/initialize main.clj: 509"
" clojure.main/init-opt main.clj: 478"
" clojure.main/load-script main.clj: 476"
" clojure.lang.Compiler.loadFile Compiler.java:8161"
" clojure.lang.Compiler.load Compiler.java:8223"
" clojure.lang.Compiler.eval Compiler.java:7747"
" clojure.lang.Compiler.eval Compiler.java:7757"
" user/eval2648 REPL Input"
" clojure.lang.Var.invoke Var.java: 390"
" clojure.lang.RestFn.invoke RestFn.java: 424"
" expand-bom/import-boms expand_bom.clj: 90"
" expand-bom/import-boms expand_bom.clj: 95"
"expand-bom/ensure-unique-dependencies! expand_bom.clj: 78"
" clojure.lang.ExceptionInfo: Conflicts in managed dependencies"
" data: {:conflicts"
" #{#{{:coordinates [org.apache.httpcomponents/httpclient \"4.5.14\"],"
" :source :manually-managed}}}}"
"clojure.lang.Compiler$CompilerException: Syntax error macroexpanding at (/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj:1:125)."
" data: #:clojure.error{:phase :execution,"
" :line 1,"
" :column 125,"
" :source"
" \"/private/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/form-init4939043928554804837.clj\"}"]
(parse-and-format "sample-clojure-exception.edn" {:print-level 10
:filter (constantly :show)}))))