|
1 | 1 | (ns clojure-sublimed.core |
2 | 2 | (:require |
3 | | - [clojure.spec.alpha :as spec] |
4 | | - [clojure.string :as str]) |
| 3 | + [clojure.spec.alpha :as spec] |
| 4 | + [clojure.string :as str]) |
5 | 5 | (:import |
6 | | - [clojure.lang Compiler Compiler$CompilerException ExceptionInfo LispReader$ReaderException] |
7 | | - [java.io BufferedWriter OutputStream OutputStreamWriter PrintWriter Writer])) |
| 6 | + [clojure.lang Compiler Compiler$CompilerException ExceptionInfo LispReader$ReaderException] |
| 7 | + [java.io BufferedWriter OutputStream OutputStreamWriter PrintWriter StringWriter Writer])) |
8 | 8 |
|
9 | 9 | (def ^:dynamic *print-quota* |
10 | 10 | 4096) |
|
45 | 45 |
|
46 | 46 | (defn bounded-pr-str [x] |
47 | 47 | (let [writer (if (> *print-quota* 0) |
48 | | - (bounded-writer (java.io.StringWriter.) *print-quota*) |
49 | | - (java.io.StringWriter.))] |
| 48 | + (bounded-writer (StringWriter.) *print-quota*) |
| 49 | + (StringWriter.))] |
50 | 50 | (try |
51 | 51 | (binding [*out* writer] |
52 | 52 | (pr x)) |
53 | 53 | (str writer) |
54 | | - (catch clojure.lang.ExceptionInfo e |
| 54 | + (catch ExceptionInfo e |
55 | 55 | (if (identical? quota-marker (ex-data e)) |
56 | 56 | (str writer "...") |
57 | 57 | (throw e)))))) |
|
78 | 78 | (.append sb cbuf ^int off ^int len)))))] |
79 | 79 | (PrintWriter. proxy true))) |
80 | 80 |
|
81 | | -;; CompilerException has location info, but its cause RuntimeException has the message ¯\_(ツ)_/¯ |
82 | | -(defn root-cause [^Throwable t] |
83 | | - (loop [t t |
84 | | - data nil] |
85 | | - (if (and |
86 | | - (nil? data) |
87 | | - (or |
88 | | - (instance? Compiler$CompilerException t) |
89 | | - (instance? LispReader$ReaderException t)) |
90 | | - (not= [0 0] ((juxt :clojure.error/line :clojure.error/column) (ex-data t)))) |
91 | | - (recur t (ex-data t)) |
92 | | - (if-some [cause (some-> t .getCause)] |
93 | | - (recur cause data) |
94 | | - (if data |
95 | | - (ExceptionInfo. "Wrapper to pass CompilerException ex-data" data t) |
96 | | - t))))) |
97 | | - |
98 | | -(defn duplicate? [^StackTraceElement prev-el ^StackTraceElement el] |
| 81 | +;; errors |
| 82 | + |
| 83 | +(defn- noise? [^StackTraceElement el] |
| 84 | + (let [class (.getClassName el)] |
| 85 | + (#{"clojure.lang.RestFn" "clojure.lang.AFn"} class))) |
| 86 | + |
| 87 | +(defn- duplicate? [^StackTraceElement prev-el ^StackTraceElement el] |
99 | 88 | (and |
100 | 89 | (= (.getClassName prev-el) (.getClassName el)) |
101 | 90 | (= (.getFileName prev-el) (.getFileName el)) |
102 | | - (= "invokeStatic" (.getMethodName prev-el)) |
103 | | - (#{"invoke" "doInvoke"} (.getMethodName el)))) |
| 91 | + (#{"invokeStatic"} (.getMethodName prev-el)) |
| 92 | + (#{"invoke" "doInvoke" "invokePrim"} (.getMethodName el)))) |
104 | 93 |
|
105 | | -(defn clear-duplicates [els] |
| 94 | +(defn- clear-duplicates [els] |
106 | 95 | (for [[prev-el el] (map vector (cons nil els) els) |
107 | 96 | :when (or (nil? prev-el) (not (duplicate? prev-el el)))] |
108 | 97 | el)) |
109 | 98 |
|
110 | | -(defn trace-element [^StackTraceElement el] |
| 99 | +(defn- trace-element [^StackTraceElement el] |
111 | 100 | (let [file (.getFileName el) |
112 | | - clojure? (or (nil? file) |
113 | | - (= file "NO_SOURCE_FILE") |
114 | | - (.endsWith file ".clj") |
115 | | - (.endsWith file ".cljc"))] |
116 | | - {:method (if clojure? |
117 | | - (Compiler/demunge (.getClassName el)) |
118 | | - (str (.getClassName el) "." (.getMethodName el))) |
119 | | - :file (.getFileName el) |
120 | | - :line (.getLineNumber el)})) |
121 | | - |
122 | | -(defn as-table [table] |
123 | | - (let [[method file] (for [col [:method :file]] |
124 | | - (->> table |
125 | | - (map #(get % col)) |
126 | | - (map str) |
127 | | - (map count) |
128 | | - (reduce max (count "null")))) |
129 | | - format-str (str "\t%-" method "s\t%-" file "s\t:%d")] |
130 | | - (->> table |
131 | | - (map #(format format-str (:method %) (:file %) (:line %))) |
132 | | - (str/join "\n")))) |
133 | | - |
134 | | -(defn trace-str |
135 | | - ([t] |
136 | | - (trace-str t nil)) |
137 | | - ([^Throwable t opts] |
138 | | - (let [{:clojure.error/keys [source line column]} (ex-data t) |
139 | | - cause (or (.getCause t) t)] |
140 | | - (str |
141 | | - (->> (.getStackTrace cause) |
142 | | - (take-while #(not (#{"clojure.lang.Compiler" "clojure.lang.LispReader"} |
143 | | - (.getClassName ^StackTraceElement %)))) |
144 | | - (remove #(#{"clojure.lang.RestFn" "clojure.lang.AFn"} (.getClassName ^StackTraceElement %))) |
145 | | - (clear-duplicates) |
146 | | - (map trace-element) |
147 | | - (reverse) |
148 | | - (as-table)) |
149 | | - "\n>>> " |
150 | | - (.getSimpleName (class cause)) |
151 | | - ": " |
152 | | - (.getMessage cause) |
153 | | - (when (:location? opts true) |
154 | | - (when (or source line column) |
155 | | - (str " (" source ":" line ":" column ")"))) |
156 | | - (when-some [data (ex-data cause)] |
157 | | - (str " " (bounded-pr-str data))))))) |
| 101 | + line (.getLineNumber el) |
| 102 | + cls (.getClassName el) |
| 103 | + method (.getMethodName el) |
| 104 | + clojure? (if file |
| 105 | + (or (.endsWith file ".clj") (.endsWith file ".cljc") (= file "NO_SOURCE_FILE")) |
| 106 | + (#{"invoke" "doInvoke" "invokePrim" "invokeStatic"} method)) |
| 107 | + |
| 108 | + [ns separator method] |
| 109 | + (cond |
| 110 | + (not clojure?) |
| 111 | + [(-> cls (str/split #"\.") last) "." method] |
| 112 | + |
| 113 | + (#{"invoke" "doInvoke" "invokeStatic"} method) |
| 114 | + (let [[ns method] (str/split (Compiler/demunge cls) #"/" 2) |
| 115 | + method (-> method |
| 116 | + (str/replace #"eval\d{3,}" "eval") |
| 117 | + (str/replace #"--\d{3,}" ""))] |
| 118 | + [ns "/" method]) |
| 119 | + |
| 120 | + :else |
| 121 | + [(Compiler/demunge cls) "/" (Compiler/demunge method)])] |
| 122 | + {:element el |
| 123 | + :file (if (= "NO_SOURCE_FILE" file) nil file) |
| 124 | + :line line |
| 125 | + :ns ns |
| 126 | + :separator separator |
| 127 | + :method method})) |
| 128 | + |
| 129 | +(defn- get-trace [^Throwable t] |
| 130 | + (->> (.getStackTrace t) |
| 131 | + (take-while |
| 132 | + (fn [^StackTraceElement el] |
| 133 | + (and |
| 134 | + (not= "clojure.lang.Compiler" (.getClassName el)) |
| 135 | + (not= "clojure.lang.LispReader" (.getClassName el)) |
| 136 | + (not (str/starts-with? (.getClassName el) "clojure_sublimed"))))) |
| 137 | + (remove noise?) |
| 138 | + (clear-duplicates) |
| 139 | + (mapv trace-element))) |
| 140 | + |
| 141 | +(defn datafy-throwable [^Throwable t] |
| 142 | + (let [trace (get-trace t) |
| 143 | + common (when-some [prev-t (.getCause t)] |
| 144 | + (let [prev-trace (get-trace prev-t)] |
| 145 | + (loop [m (dec (count trace)) |
| 146 | + n (dec (count prev-trace))] |
| 147 | + (if (and (>= m 0) (>= n 0) (= (nth trace m) (nth prev-trace n))) |
| 148 | + (recur (dec m) (dec n)) |
| 149 | + (- (dec (count trace)) m)))))] |
| 150 | + {:message (.getMessage t) |
| 151 | + :class (class t) |
| 152 | + :data (ex-data t) |
| 153 | + :trace trace |
| 154 | + :common (or common 0) |
| 155 | + :cause (some-> (.getCause t) datafy-throwable)})) |
| 156 | + |
| 157 | +(defmacro write [w & args] |
| 158 | + (list* 'do |
| 159 | + (for [arg args] |
| 160 | + (if (or (string? arg) (= String (:tag (meta arg)))) |
| 161 | + `(Writer/.write ~w ~arg) |
| 162 | + `(Writer/.write ~w (str ~arg)))))) |
| 163 | + |
| 164 | +(defn- pad [ch ^long len] |
| 165 | + (when (pos? len) |
| 166 | + (let [sb (StringBuilder. len)] |
| 167 | + (dotimes [_ len] |
| 168 | + (.append sb (char ch))) |
| 169 | + (str sb)))) |
| 170 | + |
| 171 | +(defn- split-file [s] |
| 172 | + (if-some [[_ name ext] (re-matches #"(.*)(\.[^.]+)" s)] |
| 173 | + [name ext] |
| 174 | + [s ""])) |
| 175 | + |
| 176 | +(defn- linearize [key xs] |
| 177 | + (->> xs (iterate key) (take-while some?))) |
| 178 | + |
| 179 | +(defn- longest-method [indent ts] |
| 180 | + (reduce max 0 |
| 181 | + (for [[t depth] (map vector ts (range)) |
| 182 | + el (:trace t)] |
| 183 | + (+ (* depth indent) (count (:ns el)) (count (:separator el)) (count (:method el)))))) |
| 184 | + |
| 185 | +(defn print-humanly [^Writer w ^Throwable t] |
| 186 | + (let [ts (linearize :cause (datafy-throwable t)) |
| 187 | + max-len (longest-method 0 ts) |
| 188 | + indent " "] |
| 189 | + (doseq [[idx t] (map vector (range) ts) |
| 190 | + :let [{:keys [class message data trace common]} t]] |
| 191 | + ;; class |
| 192 | + (write w (when (pos? idx) "\nCaused by: ") (.getSimpleName ^Class class)) |
| 193 | + |
| 194 | + ;; message |
| 195 | + (when message |
| 196 | + (write w ": ") |
| 197 | + (print-method message w)) |
| 198 | + |
| 199 | + ;; data |
| 200 | + (when data |
| 201 | + (write w " ") |
| 202 | + (print-method data w)) |
| 203 | + |
| 204 | + ;; trace |
| 205 | + (doseq [el (drop-last common trace) |
| 206 | + :let [{:keys [ns separator method file line]} el |
| 207 | + right-pad (pad \space (- max-len (count ns) (count separator) (count method)))]] |
| 208 | + (write w "\n" indent) |
| 209 | + |
| 210 | + ;; method |
| 211 | + (write w ns separator method) |
| 212 | + |
| 213 | + ;; locaiton |
| 214 | + (cond |
| 215 | + (= -2 line) |
| 216 | + (write w right-pad " " "Native Method") |
| 217 | + |
| 218 | + file |
| 219 | + (write w right-pad " " file " " line))) |
| 220 | + |
| 221 | + ;; ... common elements |
| 222 | + (when (pos? common) |
| 223 | + (write w "\n" indent "... " common " common elements"))))) |
| 224 | + |
| 225 | +(defn compiler-err-str [^Throwable t] |
| 226 | + (when (and |
| 227 | + (instance? Compiler$CompilerException t) |
| 228 | + (not (= :execution (:clojure.error/phase (ex-data t)))) |
| 229 | + (str/starts-with? (.getMessage t) "Syntax error") |
| 230 | + (.getCause t) |
| 231 | + (instance? RuntimeException t)) |
| 232 | + (let [cause (.getCause t) |
| 233 | + {:clojure.error/keys [source line column]} (ex-data t) |
| 234 | + source (some-> source (str/split #"/") last)] |
| 235 | + (str (.getMessage cause) " (" source ":" line ":" (some-> column inc) ")")))) |
| 236 | + |
| 237 | +(defn root-cause ^Throwable [^Throwable t] |
| 238 | + (when t |
| 239 | + (if-some [cause (.getCause t)] |
| 240 | + (recur cause) |
| 241 | + t))) |
| 242 | + |
| 243 | +(defn error-str [^Throwable t] |
| 244 | + (or |
| 245 | + (compiler-err-str t) |
| 246 | + (let [cause (root-cause t) |
| 247 | + data (ex-data cause) |
| 248 | + class (.getSimpleName (class cause)) |
| 249 | + msg (.getMessage cause)] |
| 250 | + (cond-> (str class ": " msg) |
| 251 | + data (str " " (bounded-pr-str data)))))) |
| 252 | + |
| 253 | +(defn trace-str [^Throwable t] |
| 254 | + (or |
| 255 | + (compiler-err-str t) |
| 256 | + (let [w (StringWriter.) |
| 257 | + t (if (and |
| 258 | + (instance? Compiler$CompilerException t) |
| 259 | + (= :execution (:clojure.error/phase (ex-data t)))) |
| 260 | + (.getCause t) |
| 261 | + t)] |
| 262 | + (print-humanly w t) |
| 263 | + (str w)))) |
158 | 264 |
|
159 | 265 | ;; Allow dynamic vars to be set in root thread when changed in spawned threads |
160 | 266 |
|
|
0 commit comments