-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
exceptions.clj
244 lines (193 loc) · 7.97 KB
/
exceptions.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
(ns figwheel.tools.exceptions
(:require
[clojure.string :as string]
[clojure.java.io :as io])
(:import
[java.util.regex Pattern]))
#_(remove-ns 'figwheel.tools.exceptions)
;; utils
(defn map-keys [f m]
(into {} (map (fn [[k v]]
(clojure.lang.MapEntry. (f k) v)) m)))
(defn un-clojure-error-keywords [m]
(map-keys #(if (= "clojure.error" (namespace %))
(keyword (name %))
%)
m))
(defn relativize-local [path]
(.getPath
(.relativize
(.toURI (io/file (.getCanonicalPath (io/file "."))))
;; just in case we get a URL or some such let's change it to a string first
(.toURI (io/file (str path))))))
;; compile time exceptions are syntax errors so we need to break them down into
;; message line column file
;; TODO handle spec errors
(defn cljs-analysis-ex? [tm]
(some #{:cljs/analysis-error} (keep #(get-in %[:data :tag]) (:via tm))))
(defn cljs-missing-required-ns? [tm]
(and (cljs-analysis-ex? tm)
(string? (:cause tm))
(string/starts-with? (:cause tm) "No such namespace: ")))
(defn reader-ex? [{:keys [data]}]
(= :reader-exception (:type data)))
(defn eof-reader-ex? [{:keys [data] :as tm}]
(and (reader-ex? tm) (= :eof (:ex-kind data))))
(defn cljs-failed-compiling? [tm]
(some #(.startsWith % "failed compiling file:") (keep :message (:via tm))))
(defn clj-compiler-ex? [tm]
(-> tm :via first :type pr-str (= (pr-str 'clojure.lang.Compiler$CompilerException))))
(defn clj-spec-error? [tm]
(-> tm :data :clojure.spec.alpha/problems))
(defn cljs-no-file-for-namespace? [tm]
(-> tm :via first :data :cljs.repl/error (= :invalid-ns)))
(defn exception-type? [tm]
(cond
(cljs-no-file-for-namespace? tm) :cljs/no-file-for-namespace
(cljs-missing-required-ns? tm) :cljs/missing-required-ns
(cljs-analysis-ex? tm) :cljs/analysis-error
(eof-reader-ex? tm) :tools.reader/eof-reader-exception
(reader-ex? tm) :tools.reader/reader-exception
(cljs-failed-compiling? tm) :cljs/general-compile-failure
(clj-spec-error? tm) :clj/spec-based-syntax-error
(clj-compiler-ex? tm) :clj/compiler-exception
:else nil))
(derive :clj/spec-based-syntax-error :clj/compiler-exception)
(derive :tools.reader/eof-reader-exception :tools.reader/reader-exception)
(derive :cljs/missing-required-ns :cljs/analysis-error)
(defmulti message exception-type?)
(defmethod message :default [tm] (:cause tm))
(defmethod message :tools.reader/reader-exception [tm]
(or
(some-> tm :cause (string/split #"\[line.*\]") second string/trim)
(:cause tm)))
(defmethod message :clj/spec-based-syntax-error [tm]
(first (string/split-lines (:cause tm))))
(defmethod message :cljs/no-file-for-namespace [{:keys [cause] :as tm}]
(when cause
(when-let [ns' (second (re-matches #"^(\S+).*" cause))]
(format "Could not find file for namespace '%s'
this is probably caused by a namespace/filepath miss-match
or a poorly configured classpath." ns'))))
(defmulti blame-pos exception-type?)
(defmethod blame-pos :default [tm])
(defmethod blame-pos :cljs/missing-required-ns [{:keys [cause] :as tm}]
(when-let [nmspc (and cause
(second
(re-matches #"No such namespace:\s([^,]+),.*" cause)))]
(when-let [file (-> tm :via first :data :file)]
(let [pat (Pattern/compile (str ".*" nmspc ".*"))
[pre post]
(split-with
#(not (.matches (.matcher pat %)))
(line-seq (io/reader file)))]
(when-not (empty? post)
{:line (inc (count pre))
:column (inc (.indexOf (first post) nmspc))})))))
(defmethod blame-pos :cljs/general-compile-failure [tm]
(-> (some->> tm :via reverse (filter #(or (get-in % [:data :line])
(get-in % [:data :clojure.error/line])))
first
:data
un-clojure-error-keywords)
(select-keys [:line :column])))
(defmethod blame-pos :cljs/analysis-error [tm]
(select-keys
(some->> tm :via reverse (filter #(get-in % [:data :line])) first :data)
[:line :column]))
(defmethod blame-pos :tools.reader/eof-reader-exception [tm]
(let [[line column]
(some->> tm :cause (re-matches #".*line\s(\d*)\sand\scolumn\s(\d*).*")
rest)]
(cond-> {}
line (assoc :line (Integer/parseInt line))
column (assoc :column (Integer/parseInt column)))))
(defmethod blame-pos :tools.reader/reader-exception [{:keys [data]}]
(let [{:keys [line col]} data]
(cond-> {}
line (assoc :line line)
col (assoc :column col))))
(defmethod blame-pos :clj/compiler-exception [tm]
(let [[line column]
(some->> tm :via first :message
(re-matches #"(?s).*\(.*\:(\d+)\:(\d+)\).*")
rest)]
(cond-> {}
line (assoc :line (Integer/parseInt line))
column (assoc :column (Integer/parseInt column)))))
;; return relative path because it isn't lossy
(defmulti source-file exception-type?)
(defmethod source-file :default [tm])
(defn first-file-source [tm]
(some->> tm :via (keep #(get-in % [:data :file])) first str))
(defmethod source-file :cljs/general-compile-failure [tm]
(first-file-source tm))
(defmethod source-file :cljs/analysis-error [tm]
(first-file-source tm))
(defmethod source-file :tools.reader/reader-exception [tm]
(first-file-source tm))
(defn correct-file-path [file]
(cond
(nil? file) file
(not (.exists (io/file file)))
(if-let [f (io/resource file)]
(relativize-local (.getPath f))
file)
:else (relativize-local file)))
(defmethod source-file :clj/compiler-exception [tm]
(some->> tm :via first :message (re-matches #"(?s).*\(([^:]*)\:.*") second correct-file-path))
(defmulti data exception-type?)
(defmethod data :default [tm]
(un-clojure-error-keywords (or (:data tm) (->> tm :via reverse (keep :data) first))))
#_(defmethod data :clj/spec-based-syntax-error [tm] nil)
(defn ex-type [tm]
(some-> tm :via last :type pr-str symbol))
(defn parse-exception [e]
(let [tm (if (instance? Throwable e) (Throwable->map e) e)
tag (exception-type? tm)
msg (message tm)
pos (blame-pos tm)
file (source-file tm)
ex-typ (ex-type tm)
data' (data tm)]
(cond-> (vary-meta {} assoc ::orig-throwable tm)
tag (assoc :tag tag)
msg (assoc :message msg)
pos (merge pos)
file (assoc :file file)
ex-typ (assoc :type ex-typ)
data' (assoc :data data'))))
#_(parse-exception (figwheel.tools.exceptions-test/fetch-clj-exception "(defn [])"))
;; Excerpts
(defn str-excerpt [code-str start length & [path]]
(cond->
{:start-line start
:excerpt (->> (string/split-lines code-str)
(drop (dec start))
(take length)
(string/join "\n"))}
path (assoc :path path)))
(defn file-excerpt [file start length]
(str-excerpt (slurp file) start length (.getCanonicalPath file)))
(defn root-source->file-excerpt [{:keys [source-form] :as root-source-info} except-data]
(let [{:keys [source column]} (when (instance? clojure.lang.IMeta source-form)
(meta source-form))]
(cond-> except-data
(and column (> column 1) (= (:line except-data) 1) (:column except-data))
(update :column #(max 1 (- % (dec column))))
source (assoc :file-excerpt {:start-line 1 :excerpt source}))))
(defn add-excerpt
([parsed] (add-excerpt parsed nil))
([{:keys [file line data] :as parsed} code-str]
(cond
(and line file (.isFile (io/file file)))
(let [fex (file-excerpt (io/file file) (max 1 (- line 10)) 20)]
(cond-> parsed
fex (assoc :file-excerpt fex)))
(and line (:root-source-info data))
(root-source->file-excerpt (:root-source-info data) parsed)
(and line code-str)
(let [str-ex (str-excerpt code-str (max 1 (- line 10)) 20)]
(cond-> parsed
str-ex (assoc :file-excerpt str-ex)))
:else parsed)))