-
Notifications
You must be signed in to change notification settings - Fork 51
/
Copy pathhints.cljs
155 lines (128 loc) · 6.55 KB
/
hints.cljs
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
(ns devtools.hints
(:require-macros [devtools.compiler :refer [check-compiler-options!]]
[devtools.oops :refer [unchecked-aget]])
(:require [devtools.prefs :refer [pref]]
[devtools.context :as context]
[cljs.stacktrace :as stacktrace]))
; cljs.stacktrace does not play well in :advanced mode optimizations, see https://github.com/binaryage/cljs-devtools/issues/37
(check-compiler-options!)
(defn ^:dynamic available? []
true)
; Question: How much time have you lost staring at "Cannot read property 'call' of null" kind of errors?
;
; ---------------------------------------------------------------------------------------------------------------------------
;
; The idea is to try enhance error object's .stack and .message fields with additional info about
; the call site causing null type error. With optimizations :none the name of the null call site can be seen.
;
; The enhancing handler function tries to:
; 1) parse error's stack trace.
; 2) look original javascript source file up (via sync AJAX fetch by default).
; 3) locate reported line and column.
; 4) presents problematic line with a column hint as addition to .stack or .message strings.
; Technically the trick here is to override TypeError.prototype.toString
; and global window.onerror handler to enhance uncaught errors.
;
; With that we should handle two situations:
; 1) either error gets printed (typically in user's catch via console), so patched toString() method gets called.
; 2) or it is uncaught and our global error handler should take care of possible enhancement
; before devtools present it to the user themselves.
;
; note: Tested under Chrome only
(def ^:dynamic *installed* false)
(def ^:dynamic *original-global-error-handler* nil)
(def ^:dynamic *original-type-error-prototype-to-string* nil)
(def processed-errors (volatile! nil))
; ---------------------------------------------------------------------------------------------------------------------------
(defn set-processed-errors! [val]
(vreset! processed-errors val))
(defn get-processed-errors! []
(if-let [val @processed-errors]
val
(if (exists? js/WeakSet)
(set-processed-errors! (js/WeakSet.)))))
; ---------------------------------------------------------------------------------------------------------------------------
(defn empty-as-nil [str]
(if (empty? str) nil str))
(defn ajax-reader [url]
(let [xhr (js/XMLHttpRequest.)]
(.open xhr "GET" url false)
(.send xhr)
(empty-as-nil (.-responseText xhr))))
(defn retrieve-javascript-source [where]
(let [reader (or (pref :file-reader) ajax-reader)]
(reader where)))
(defn get-line [lines line-number]
(unchecked-aget lines (dec line-number))) ; line numbering is 1-based
(defn extend-content [content lines line-number min-length]
(if (or (> (count content) min-length)
(not (pos? line-number)))
content
(let [prev-line-number (dec line-number)
prev-line (get-line lines prev-line-number)
new-content (str prev-line "\n" content)]
(extend-content new-content lines prev-line-number min-length))))
(defn mark-call-closed-at-column [line column]
(let [n (dec column) ; column number is 1-based
prefix (.substring line 0 n)
postfix (.substring line n)]
(str prefix " <<< ☢ NULL ☢ <<< " postfix)))
(defn mark-null-call-site-location [file line-number column]
(let [content (retrieve-javascript-source file)
lines (.split content "\n")
line (get-line lines line-number)
marked-line (mark-call-closed-at-column line column)
min-length (or (pref :sanity-hint-min-length) 128)]
(extend-content marked-line lines line-number min-length)))
(defn make-sense-of-the-error [message file line-number column]
(cond
(re-matches #"Cannot read property 'call' of.*" message) (mark-null-call-site-location file line-number column)
:else nil))
(defn parse-stacktrace [native-stack-trace]
(stacktrace/parse-stacktrace {} native-stack-trace {:ua-product :chrome} {:asset-root ""}))
(defn error-object-sense [error]
(try
(let [native-stack-trace (.-stack error)
stack-trace (parse-stacktrace native-stack-trace)
top-item (second stack-trace) ; first line is just an error message
{:keys [file line column]} top-item]
(make-sense-of-the-error (.-message error) file line column))
(catch :default _e
; silently fail in case of troubles parsing stack trace
false)))
(defn type-error-to-string [self]
(if-let [seen-errors (get-processed-errors!)]
(when-not (.has seen-errors self)
(.add seen-errors self)
(when-let [sense (error-object-sense self)]
(set! (.-message self) (str (.-message self) ", a sanity hint:\n" sense))))) ; this is dirty, patch message field before it gets used
(.call *original-type-error-prototype-to-string* self))
(defn global-error-handler [message url line column error]
(let [res (if *original-global-error-handler*
(*original-global-error-handler* message url line column error))]
(if-not res
(when-let [sense (error-object-sense error)]
(.info (context/get-console) "A sanity hint for incoming uncaught error:\n" sense)
false)
true)))
(defn install-type-error-enhancer []
(set! *original-global-error-handler* (.-onerror (context/get-root)))
(set! (.-onerror (context/get-root)) global-error-handler)
(let [prototype (.-prototype js/TypeError)]
(set! *original-type-error-prototype-to-string* (.-toString prototype))
(set! (.-toString prototype) #(this-as self (type-error-to-string self))))) ; work around http://dev.clojure.org/jira/browse/CLJS-1545
; -- installation -----------------------------------------------------------------------------------------------------------
(defn installed? []
*installed*)
(defn install! []
(when-not *installed*
(set! *installed* true)
(install-type-error-enhancer)
true))
(defn uninstall! []
(when *installed*
(set! *installed* false)
(assert *original-type-error-prototype-to-string*)
(set! (.-onerror (context/get-root)) *original-global-error-handler*)
(let [prototype (.-prototype js/TypeError)]
(set! (.-toString prototype) *original-type-error-prototype-to-string*))))