Skip to content

Commit 8463660

Browse files
committed
Add filtering of stack frames
Also, changed the signatures of format-exception and write-exception, adding arities, and making the final parameter a simple, optional map. Fixes #25
1 parent 8fd31d1 commit 8463660

File tree

3 files changed

+121
-41
lines changed

3 files changed

+121
-41
lines changed

src/io/aviso/exception.clj

Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
(defn- expand-exception
5656
[^Throwable exception]
5757
(let [properties (bean exception)
58-
cause (:cause properties)
5958
nil-property-keys (match-keys properties nil?)
6059
throwable-property-keys (match-keys properties #(.isInstance Throwable %))
6160
remove' #(remove %2 %1)
@@ -168,34 +167,73 @@
168167

169168
(def ^:dynamic *fonts*
170169
"ANSI fonts for different elements in the formatted exception report."
171-
{:exception bold-red-font
172-
:reset reset-font
173-
:message italic-font
174-
:property bold-font
175-
:source green-font
176-
:function-name bold-yellow-font
177-
:clojure-frame yellow-font
178-
:java-frame white-font})
170+
{:exception bold-red-font
171+
:reset reset-font
172+
:message italic-font
173+
:property bold-font
174+
:source green-font
175+
:function-name bold-yellow-font
176+
:clojure-frame yellow-font
177+
:java-frame white-font
178+
:omiitted-frame white-font})
179179

180180
(defn- preformat-stack-frame
181-
[element]
182-
(let [names (:names element)]
183-
(if (-> element :names empty?)
184-
;; Case 1: names is empty, it's a Java frame
185-
(let [full-name (str (:class element) "." (:method element))]
186-
(assoc element
187-
:formatted-name (str (:java-frame *fonts*) full-name (:reset *fonts*))))
188-
;; Case 2: it's a Clojure name
189-
(assoc element
190-
:formatted-name (str
191-
(:clojure-frame *fonts*)
192-
(->> names drop-last (str/join "/"))
193-
"/"
194-
(:function-name *fonts*) (last names) (:reset *fonts*))))))
181+
[frame]
182+
(cond
183+
(:omitted frame)
184+
(assoc frame :formatted-name (str (:omitted-frame *fonts*) "..." (:reset *fonts*))
185+
:file ""
186+
:line nil)
187+
188+
;; When :names is empty, it's a Java (not Clojure) frame
189+
(-> frame :names empty?)
190+
(let [full-name (str (:class frame) "." (:method frame))
191+
formatted-name (str (:java-frame *fonts*) full-name (:reset *fonts*))]
192+
(assoc frame
193+
:formatted-name formatted-name))
194+
195+
:else
196+
(let [names (:names frame)
197+
formatted-name (str
198+
(:clojure-frame *fonts*)
199+
(->> names drop-last (str/join "/"))
200+
"/"
201+
(:function-name *fonts*) (last names) (:reset *fonts*))]
202+
(assoc frame :formatted-name formatted-name))))
203+
204+
(defn- apply-frame-filter
205+
[frame-filter frames]
206+
(if (nil? frame-filter)
207+
frames
208+
(loop [result []
209+
[frame & more-frames] frames
210+
omitting false]
211+
(case (if frame (frame-filter frame) :terminate)
212+
213+
:terminate
214+
result
215+
216+
:show
217+
(recur (conj result frame)
218+
more-frames
219+
false)
220+
221+
:hide
222+
(recur result more-frames omitting)
223+
224+
:omit
225+
(if omitting
226+
(recur result more-frames true)
227+
(recur (conj result (assoc frame :omitted true))
228+
more-frames
229+
true))))))
195230

196231
(defn- write-stack-trace
197-
[writer exception frame-limit]
198-
(let [elements (->> exception expand-stack-trace (map preformat-stack-frame))
232+
[writer exception frame-limit frame-filter]
233+
(let [elements (->> exception
234+
expand-stack-trace
235+
(apply-frame-filter frame-filter)
236+
(map preformat-stack-frame))
199237
elements' (if frame-limit (take frame-limit elements) elements)
200238
formatter (c/format-columns [:right (c/max-value-length elements' :formatted-name)]
201239
" " (:source *fonts*)
@@ -217,19 +255,43 @@
217255
"Writes a formatted version of the exception to the writer. By default, writes to *out* and includes
218256
the stack trace, with no frame limit.
219257
258+
A frame filter may be specified; the frame filter is passed the stack frame maps; for each map
259+
it may return one of :show, :hide, :omit, or :terminate. A stack frame map will have keys :file, :line, :class,
260+
:package, :simple-class, :method, :name, and :names.
261+
262+
:file, :class, :package, :simple-class, :method, and :name are all strings. :line is an integer. :file and :line
263+
are sometimes omitted.
264+
265+
:name is the Clojure name for the frame, or blank for a Java frame. :names is a vector of
266+
strings representing first the namespace name, then the top-level function name, then nested function names
267+
(which are often \"fn\" for anonymous functions). This is broken out primarily for rendering (so that the
268+
last function name can be presented in bold), but may still be useful.
269+
270+
:show is the normal state; display the stack frame.
271+
:hide prevents the frame from being displayed, as if it never existed.
272+
:omit replaces the frame with a \"...\" placeholder; multiple consecutive :omits will be collapsed to a single line.
273+
Use :omit for \"uninteresting\" stack frames.
274+
:terminate hides the frame AND all later frames.
275+
276+
The default is no filter; however the io.aviso.repl namespace does supply a standard filter.
277+
220278
When set, the frame limit is the number of stack frames to display; if non-nil, then some of the outer-most
221279
stack frames may be omitted. It may be set to 0 to omit the stack trace entirely (but still display
222-
the exception stack).
280+
the exception stack). The frame limit applies after the frame filter has been applied.
223281
224282
Properties of exceptions will be output using Clojure's pretty-printer, honoring all of the normal vars used
225283
to configure pretty-printing; however, if *print-length* is left as its default (nil), the print length will be set to 10.
226284
This is to ensure that infinite lists do not cause endless output or other exceptions.
227285
228286
The *fonts* var contains ANSI definitions for how fonts are displayed; bind it to nil to remove ANSI formatting entirely."
229-
([exception] (write-exception *out* exception))
230-
([writer exception & {show-properties? :properties
231-
frame-limit :frame-limit
232-
:or {show-properties? true}}]
287+
([exception]
288+
(write-exception *out* exception))
289+
([writer exception]
290+
(write-exception writer exception nil))
291+
([writer exception {show-properties? :properties
292+
frame-limit :frame-limit
293+
frame-filter :filter
294+
:or {show-properties? true}}]
233295
(let [exception-font (:exception *fonts*)
234296
message-font (:message *fonts*)
235297
property-font (:property *fonts*)
@@ -261,10 +323,12 @@
261323
(str property-font k reset-font)
262324
(-> properties (get k) format-property-value)))))
263325
(if (:root e)
264-
(write-stack-trace writer exception frame-limit)))))
326+
(write-stack-trace writer exception frame-limit frame-filter)))))
265327
(w/flush-writer writer)))
266328

267329
(defn format-exception
268330
"Formats an exception as a multi-line string using write-exception."
269-
[exception & options]
270-
(apply w/into-string write-exception exception options))
331+
([exception]
332+
(format-exception exception nil))
333+
([exception options]
334+
(w/into-string write-exception exception options)))

src/io/aviso/repl.clj

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,46 @@
88
[repl :as repl]
99
[stacktrace :as st]]))
1010

11+
(defn standard-frame-filter
12+
"Default stack frame filter used when printing REPL exceptions. This will omit frames in the `clojure.lang` package,
13+
and terminates the stack trace at the read-eval-print loop frame. This tends to be very concise; you can use
14+
`(write-exception *e)` to display the full stack trace."
15+
[frame]
16+
(cond
17+
(-> frame :package (= "clojure.lang"))
18+
:omit
19+
20+
(-> frame :name (.startsWith "clojure.main/repl/read-eval-print/"))
21+
:terminate
22+
23+
:else
24+
:show))
25+
1126
(defn- reset-var!
1227
[v override]
1328
(alter-var-root v (constantly override)))
1429

1530
(defn- write
16-
[e & options]
17-
(print (apply format-exception e options))
31+
[e options]
32+
(print (format-exception e (assoc options :filter standard-frame-filter)))
1833
(flush))
1934

2035
(defn pretty-repl-caught
2136
"A replacement for clojure.main/repl-caught that prints the exception to *err*, without a stack trace or properties."
2237
[e]
23-
(write e :frame-limit 0 :properties false))
38+
(write e {:frame-limit 0 :properties false}))
2439

2540
(defn pretty-pst
2641
"Used as an override of clojure.repl/pst but uses pretty formatting. The optional parameter must be an exception
2742
(it can not be a depth, as with the real pst)."
2843
([] (pretty-pst *e))
29-
([e] (write e)))
44+
([e] (write e nil)))
3045

3146
(defn pretty-print-stack-trace
3247
"Replacement for clojure.stracktrace/print-stack-trace and print-cause-trace."
3348
([tr] (pretty-print-stack-trace tr nil))
3449
([tr n]
35-
(write tr :frame-limit n)))
50+
(write tr {:frame-limit n})))
3651

3752
(defn install-pretty-exceptions
3853
"Installs an override that outputs pretty exceptions when caught by the main REPL loop. Also, overrides

test/user.clj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
(ns user
2-
(import (java.sql SQLException))
3-
(use [io.aviso ansi binary exception]
1+
(ns user (import (java.sql SQLException))
2+
(use [io.aviso ansi binary exception repl]
43
[clojure test pprint]))
54

5+
(install-pretty-exceptions)
6+
67
(defn- jdbc-update
78
[]
89
(throw (SQLException. "Database failure\nSELECT FOO, BAR, BAZ\nFROM GNIP\nfailed with ABC123" "ABC" 123)))

0 commit comments

Comments
 (0)