692 lines
30 KiB
Clojure
692 lines
30 KiB
Clojure
;; Copyright (c) Rich Hickey. All rights reserved.
|
|
;; The use and distribution terms for this software are covered by the
|
|
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
|
|
;; which can be found in the file epl-v10.html at the root of this distribution.
|
|
;; By using this software in any fashion, you are agreeing to be bound by
|
|
;; the terms of this license.
|
|
;; You must not remove this notice, or any other, from this software.
|
|
|
|
(ns cljs.stacktrace
|
|
(:require #?@(:clj [[cljs.util :as util]
|
|
[clojure.java.io :as io]]
|
|
:cljs [[goog.string :as gstring]])
|
|
[clojure.string :as string])
|
|
#?(:clj (:import [java.util.regex Pattern]
|
|
[java.io File])))
|
|
|
|
(defmulti parse-stacktrace
|
|
"Parse a JavaScript stacktrace string into a canonical data form. The
|
|
arguments:
|
|
|
|
repl-env - the repl environment, an optional map with :host and :port keys
|
|
if the stacktrace includes url, not file references
|
|
st - the original stacktrace string to parse
|
|
err - an error map. :ua-product key defines the type of stacktrace parser
|
|
to use, for example :chrome
|
|
opts - additional options. :output-dir maybe given in this argument if
|
|
:host and :port do not apply, for example, a file path
|
|
|
|
The canonical stacktrace representation can easily be mapped to a
|
|
ClojureScript one see mapped-stacktrace and mapped-stacktrace-str"
|
|
(fn [repl-env st err opts] (:ua-product err)))
|
|
|
|
(defn parse-int [s]
|
|
#?(:clj (Long/parseLong s)
|
|
:cljs (js/parseInt s 10)))
|
|
|
|
(defn starts-with?
|
|
#?(:cljs {:tag boolean})
|
|
[^String s0 s1]
|
|
#?(:clj (.startsWith s0 s1)
|
|
:cljs (gstring/startsWith s0 s1)))
|
|
|
|
(defn ends-with?
|
|
#?(:cljs {:tag boolean})
|
|
[^String s0 s1]
|
|
#?(:clj (.endsWith s0 s1)
|
|
:cljs (gstring/endsWith s0 s1)))
|
|
|
|
(defn string->regex [s]
|
|
#?(:clj (Pattern/compile s)
|
|
:cljs (js/RegExp. s)))
|
|
|
|
(defn output-directory [opts]
|
|
#?(:clj (util/output-directory opts)
|
|
:cljs (or (:output-dir opts) "out")))
|
|
|
|
(defmethod parse-stacktrace :default
|
|
[repl-env st err opts] st)
|
|
|
|
(defn parse-file-line-column [flc]
|
|
(if-not (re-find #":" flc)
|
|
[flc nil nil]
|
|
(let [xs (string/split flc #":")
|
|
[pre [line column]]
|
|
(reduce
|
|
(fn [[pre post] [x i]]
|
|
(if (<= i 2)
|
|
[pre (conj post x)]
|
|
[(conj pre x) post]))
|
|
[[] []] (map vector xs (range (count xs) 0 -1)))
|
|
file (string/join ":" pre)]
|
|
[(cond-> file
|
|
(starts-with? file "(") (string/replace "(" ""))
|
|
(parse-int
|
|
(cond-> line
|
|
(ends-with? line ")") (string/replace ")" "")))
|
|
(parse-int
|
|
(cond-> column
|
|
(ends-with? column ")") (string/replace ")" "")))])))
|
|
|
|
(defn parse-file
|
|
"Given a browser file url convert it into a relative path that can be used
|
|
to locate the original source."
|
|
[{:keys [host host-port port] :as repl-env} file {:keys [asset-path] :as opts}]
|
|
(let [urlpat (if host
|
|
(string->regex
|
|
(str "http://" host ":" (or host-port port) "/"))
|
|
"")
|
|
match (if host
|
|
(re-find urlpat file)
|
|
(contains? opts :output-dir))]
|
|
(if match
|
|
(-> file
|
|
(string/replace urlpat "")
|
|
(string/replace
|
|
(string->regex
|
|
;; if :asset-path specified drop leading slash
|
|
(str "^" (or (and asset-path (string/replace asset-path #"^/" ""))
|
|
(output-directory opts)) "/"))
|
|
""))
|
|
(if-let [asset-root (:asset-root opts)]
|
|
(string/replace file asset-root "")
|
|
(throw
|
|
(ex-info (str "Could not relativize URL " file)
|
|
{:type :parse-stacktrace
|
|
:reason :relativize-url}))))))
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Chrome Stacktrace
|
|
|
|
(defn chrome-st-el->frame
|
|
[repl-env st-el opts]
|
|
(let [xs (-> st-el
|
|
(string/replace #"\s+at\s+" "")
|
|
(string/split #"\s+"))
|
|
[function flc] (if (== 1 (count xs))
|
|
[nil (first xs)]
|
|
[(first xs) (last xs)])
|
|
[file line column] (parse-file-line-column flc)]
|
|
(if (and file function line column)
|
|
{:file (parse-file repl-env file opts)
|
|
:function (string/replace function #"Object\." "")
|
|
:line line
|
|
:column column}
|
|
(when-not (string/blank? function)
|
|
{:file nil
|
|
:function (string/replace function #"Object\." "")
|
|
:line nil
|
|
:column nil}))))
|
|
|
|
(comment
|
|
(chrome-st-el->frame {:host "localhost" :port 9000}
|
|
"\tat cljs$core$ffirst (http://localhost:9000/out/cljs/core.js:5356:34)" {})
|
|
)
|
|
|
|
(defmethod parse-stacktrace :chrome
|
|
[repl-env st err opts]
|
|
(->> st
|
|
string/split-lines
|
|
(drop-while #(starts-with? % "Error"))
|
|
(take-while #(not (starts-with? % " at eval")))
|
|
(map #(chrome-st-el->frame repl-env % opts))
|
|
(remove nil?)
|
|
vec))
|
|
|
|
(comment
|
|
(parse-stacktrace {:host "localhost" :port 9000}
|
|
"Error: 1 is not ISeqable
|
|
at Object.cljs$core$seq [as seq] (http://localhost:9000/out/cljs/core.js:4258:8)
|
|
at Object.cljs$core$first [as first] (http://localhost:9000/out/cljs/core.js:4288:19)
|
|
at cljs$core$ffirst (http://localhost:9000/out/cljs/core.js:5356:34)
|
|
at http://localhost:9000/out/cljs/core.js:16971:89
|
|
at cljs.core.map.cljs$core$map__2 (http://localhost:9000/out/cljs/core.js:16972:3)
|
|
at http://localhost:9000/out/cljs/core.js:10981:129
|
|
at cljs.core.LazySeq.sval (http://localhost:9000/out/cljs/core.js:10982:3)
|
|
at cljs.core.LazySeq.cljs$core$ISeqable$_seq$arity$1 (http://localhost:9000/out/cljs/core.js:11073:10)
|
|
at Object.cljs$core$seq [as seq] (http://localhost:9000/out/cljs/core.js:4239:13)
|
|
at Object.cljs$core$pr_sequential_writer [as pr_sequential_writer] (http://localhost:9000/out/cljs/core.js:28706:14)"
|
|
{:ua-product :chrome}
|
|
nil)
|
|
|
|
(parse-stacktrace {:host "localhost" :port 9000}
|
|
"Error: 1 is not ISeqable
|
|
at Object.cljs$core$seq [as seq] (http://localhost:9000/js/cljs/core.js:4258:8)
|
|
at Object.cljs$core$first [as first] (http://localhost:9000/js/cljs/core.js:4288:19)
|
|
at cljs$core$ffirst (http://localhost:9000/js/cljs/core.js:5356:34)
|
|
at http://localhost:9000/js/cljs/core.js:16971:89
|
|
at cljs.core.map.cljs$core$map__2 (http://localhost:9000/js/cljs/core.js:16972:3)
|
|
at http://localhost:9000/js/cljs/core.js:10981:129
|
|
at cljs.core.LazySeq.sval (http://localhost:9000/js/cljs/core.js:10982:3)
|
|
at cljs.core.LazySeq.cljs$core$ISeqable$_seq$arity$1 (http://localhost:9000/js/cljs/core.js:11073:10)
|
|
at Object.cljs$core$seq [as seq] (http://localhost:9000/js/cljs/core.js:4239:13)
|
|
at Object.cljs$core$pr_sequential_writer [as pr_sequential_writer] (http://localhost:9000/js/cljs/core.js:28706:14)"
|
|
{:ua-product :chrome}
|
|
{:asset-path "/js"})
|
|
|
|
(parse-stacktrace {:host "localhost" :port 9000}
|
|
"Error: 1 is not ISeqable
|
|
at Object.cljs$core$seq [as seq] (http://localhost:9000/out/cljs/core.js:4259:8)
|
|
at Object.cljs$core$first [as first] (http://localhost:9000/out/cljs/core.js:4289:19)
|
|
at cljs$core$ffirst (http://localhost:9000/out/cljs/core.js:5357:18)
|
|
at eval (eval at <anonymous> (http://localhost:9000/out/clojure/browser/repl.js:23:272), <anonymous>:1:106)
|
|
at eval (eval at <anonymous> (http://localhost:9000/out/clojure/browser/repl.js:23:272), <anonymous>:9:3)
|
|
at eval (eval at <anonymous> (http://localhost:9000/out/clojure/browser/repl.js:23:272), <anonymous>:14:4)
|
|
at http://localhost:9000/out/clojure/browser/repl.js:23:267
|
|
at clojure$browser$repl$evaluate_javascript (http://localhost:9000/out/clojure/browser/repl.js:26:4)
|
|
at Object.callback (http://localhost:9000/out/clojure/browser/repl.js:121:169)
|
|
at goog.messaging.AbstractChannel.deliver (http://localhost:9000/out/goog/messaging/abstractchannel.js:142:13)"
|
|
{:ua-product :chrome}
|
|
nil)
|
|
|
|
;; Node.js example
|
|
(parse-stacktrace {}
|
|
"Error: 1 is not ISeqable
|
|
at Object.cljs$core$seq [as seq] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:3999:8)
|
|
at Object.cljs$core$first [as first] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:4018:19)
|
|
at cljs$core$ffirst (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:5161:34)
|
|
at /home/my/cool/project/.cljs_bootstrap/cljs/core.js:16006:88
|
|
at cljs.core.map.cljs$core$IFn$_invoke$arity$2 (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:16007:3)
|
|
at cljs.core.LazySeq.sval (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:10244:109)
|
|
at cljs.core.LazySeq.cljs$core$ISeqable$_seq$arity$1 (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:10335:10)
|
|
at Object.cljs$core$seq [as seq] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:3980:13)
|
|
at Object.cljs$core$pr_sequential_writer [as pr_sequential_writer] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:28084:14)
|
|
at cljs.core.LazySeq.cljs$core$IPrintWithWriter$_pr_writer$arity$3 (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:28812:18)"
|
|
{:ua-product :chrome}
|
|
{:output-dir "/home/my/cool/project/.cljs_bootstrap"})
|
|
)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Safari Stacktrace
|
|
|
|
(defn safari-st-el->frame
|
|
[repl-env st-el opts]
|
|
(let [[function flc] (if (re-find #"@" st-el)
|
|
(string/split st-el #"@")
|
|
[nil st-el])
|
|
[file line column] (parse-file-line-column flc)]
|
|
(if (and file function line column)
|
|
{:file (parse-file repl-env file opts)
|
|
:function (string/trim function)
|
|
:line line
|
|
:column column}
|
|
(when-not (string/blank? function)
|
|
{:file nil
|
|
:function (string/trim function)
|
|
:line nil
|
|
:column nil}))))
|
|
|
|
(comment
|
|
(safari-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs$core$seq@http://localhost:9000/out/cljs/core.js:4259:17" {})
|
|
|
|
(safari-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs$core$seq@http://localhost:9000/js/cljs/core.js:4259:17" {:asset-path "js"})
|
|
)
|
|
|
|
(defmethod parse-stacktrace :safari
|
|
[repl-env st err opts]
|
|
(->> st
|
|
string/split-lines
|
|
(drop-while #(starts-with? % "Error"))
|
|
(take-while #(not (starts-with? % "eval code")))
|
|
(remove string/blank?)
|
|
(map #(safari-st-el->frame repl-env % opts))
|
|
(remove nil?)
|
|
vec))
|
|
|
|
(comment
|
|
(parse-stacktrace {}
|
|
"cljs$core$seq@out/cljs/core.js:3999:17
|
|
cljs$core$first@out/cljs/core.js:4018:22
|
|
cljs$core$ffirst@out/cljs/core.js:5161:39
|
|
global code"
|
|
{:ua-product :safari}
|
|
{:output-dir "out"})
|
|
|
|
(parse-stacktrace {:host "localhost" :port 9000}
|
|
"cljs$core$seq@http://localhost:9000/out/cljs/core.js:4259:17
|
|
cljs$core$first@http://localhost:9000/out/cljs/core.js:4289:22
|
|
cljs$core$ffirst@http://localhost:9000/out/cljs/core.js:5357:39
|
|
http://localhost:9000/out/cljs/core.js:16972:92
|
|
http://localhost:9000/out/cljs/core.js:16973:3
|
|
http://localhost:9000/out/cljs/core.js:10982:133
|
|
sval@http://localhost:9000/out/cljs/core.js:10983:3
|
|
cljs$core$ISeqable$_seq$arity$1@http://localhost:9000/out/cljs/core.js:11074:14
|
|
cljs$core$seq@http://localhost:9000/out/cljs/core.js:4240:44
|
|
cljs$core$pr_sequential_writer@http://localhost:9000/out/cljs/core.js:28707:17
|
|
cljs$core$IPrintWithWriter$_pr_writer$arity$3@http://localhost:9000/out/cljs/core.js:29386:38
|
|
cljs$core$pr_writer_impl@http://localhost:9000/out/cljs/core.js:28912:57
|
|
cljs$core$pr_writer@http://localhost:9000/out/cljs/core.js:29011:32
|
|
cljs$core$pr_seq_writer@http://localhost:9000/out/cljs/core.js:29015:20
|
|
cljs$core$pr_sb_with_opts@http://localhost:9000/out/cljs/core.js:29078:24
|
|
cljs$core$pr_str_with_opts@http://localhost:9000/out/cljs/core.js:29092:48
|
|
cljs$core$pr_str__delegate@http://localhost:9000/out/cljs/core.js:29130:34
|
|
cljs$core$pr_str@http://localhost:9000/out/cljs/core.js:29139:39
|
|
eval code
|
|
eval@[native code]
|
|
http://localhost:9000/out/clojure/browser/repl.js:23:271
|
|
clojure$browser$repl$evaluate_javascript@http://localhost:9000/out/clojure/browser/repl.js:26:4
|
|
http://localhost:9000/out/clojure/browser/repl.js:121:173
|
|
deliver@http://localhost:9000/out/goog/messaging/abstractchannel.js:142:21
|
|
xpcDeliver@http://localhost:9000/out/goog/net/xpc/crosspagechannel.js:733:19
|
|
messageReceived_@http://localhost:9000/out/goog/net/xpc/nativemessagingtransport.js:321:23
|
|
fireListener@http://localhost:9000/out/goog/events/events.js:741:25
|
|
handleBrowserEvent_@http://localhost:9000/out/goog/events/events.js:862:34
|
|
http://localhost:9000/out/goog/events/events.js:276:42"
|
|
{:ua-product :safari}
|
|
nil)
|
|
)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Firefox Stacktrace
|
|
|
|
(defn firefox-clean-function [f]
|
|
(as-> f f
|
|
(cond
|
|
(string/blank? f) nil
|
|
(not= (.indexOf f "</") -1)
|
|
(let [idx (.indexOf f "</")]
|
|
(.substring f (+ idx 2)))
|
|
:else f)
|
|
(-> f
|
|
(string/replace #"<" "")
|
|
(string/replace #?(:clj #"\/" :cljs (js/RegExp. "\\/")) ""))))
|
|
|
|
(defn firefox-st-el->frame
|
|
[repl-env st-el opts]
|
|
(let [[function flc] (if (re-find #"@" st-el)
|
|
(string/split st-el #"@")
|
|
[nil st-el])
|
|
[file line column] (parse-file-line-column flc)]
|
|
(if (and file function line column)
|
|
{:file (parse-file repl-env file opts)
|
|
:function (firefox-clean-function function)
|
|
:line line
|
|
:column column}
|
|
(when-not (string/blank? function)
|
|
{:file nil
|
|
:function (firefox-clean-function function)
|
|
:line nil
|
|
:column nil}))))
|
|
|
|
(comment
|
|
(firefox-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs$core$seq@http://localhost:9000/out/cljs/core.js:4258:8" {})
|
|
|
|
(firefox-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs.core.map</cljs$core$map__2/</<@http://localhost:9000/out/cljs/core.js:16971:87" {})
|
|
|
|
(firefox-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs.core.map</cljs$core$map__2/</<@http://localhost:9000/out/cljs/core.js:16971:87" {})
|
|
|
|
(firefox-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs.core.pr_str</cljs$core$pr_str@http://localhost:9000/out/cljs/core.js:29138:8" {})
|
|
|
|
(firefox-st-el->frame {:host "localhost" :port 9000}
|
|
"cljs.core.pr_str</cljs$core$pr_str__delegate@http://localhost:9000/out/cljs/core.js:29129:8" {})
|
|
)
|
|
|
|
(defmethod parse-stacktrace :firefox
|
|
[repl-env st err opts]
|
|
(->> st
|
|
string/split-lines
|
|
(drop-while #(starts-with? % "Error"))
|
|
(take-while #(= (.indexOf % "> eval") -1))
|
|
(remove string/blank?)
|
|
(map #(firefox-st-el->frame repl-env % opts))
|
|
(remove nil?)
|
|
vec))
|
|
|
|
(comment
|
|
(parse-stacktrace {:host "localhost" :port 9000}
|
|
"cljs$core$seq@http://localhost:9000/out/cljs/core.js:4258:8
|
|
cljs$core$first@http://localhost:9000/out/cljs/core.js:4288:9
|
|
cljs$core$ffirst@http://localhost:9000/out/cljs/core.js:5356:24
|
|
cljs.core.map</cljs$core$map__2/</<@http://localhost:9000/out/cljs/core.js:16971:87
|
|
cljs.core.map</cljs$core$map__2/<@http://localhost:9000/out/cljs/core.js:16970:1
|
|
cljs.core.LazySeq.prototype.sval/self__.s<@http://localhost:9000/out/cljs/core.js:10981:119
|
|
cljs.core.LazySeq.prototype.sval@http://localhost:9000/out/cljs/core.js:10981:13
|
|
cljs.core.LazySeq.prototype.cljs$core$ISeqable$_seq$arity$1@http://localhost:9000/out/cljs/core.js:11073:1
|
|
cljs$core$seq@http://localhost:9000/out/cljs/core.js:4239:8
|
|
cljs$core$pr_sequential_writer@http://localhost:9000/out/cljs/core.js:28706:4
|
|
cljs.core.LazySeq.prototype.cljs$core$IPrintWithWriter$_pr_writer$arity$3@http://localhost:9000/out/cljs/core.js:29385:8
|
|
cljs$core$pr_writer_impl@http://localhost:9000/out/cljs/core.js:28911:8
|
|
cljs$core$pr_writer@http://localhost:9000/out/cljs/core.js:29010:8
|
|
cljs$core$pr_seq_writer@http://localhost:9000/out/cljs/core.js:29014:1
|
|
cljs$core$pr_sb_with_opts@http://localhost:9000/out/cljs/core.js:29077:1
|
|
cljs$core$pr_str_with_opts@http://localhost:9000/out/cljs/core.js:29091:23
|
|
cljs.core.pr_str</cljs$core$pr_str__delegate@http://localhost:9000/out/cljs/core.js:29129:8
|
|
cljs.core.pr_str</cljs$core$pr_str@http://localhost:9000/out/cljs/core.js:29138:8
|
|
@http://localhost:9000/out/clojure/browser/repl.js line 23 > eval:1:25
|
|
@http://localhost:9000/out/clojure/browser/repl.js line 23 > eval:1:2
|
|
clojure$browser$repl$evaluate_javascript/result<@http://localhost:9000/out/clojure/browser/repl.js:23:267
|
|
clojure$browser$repl$evaluate_javascript@http://localhost:9000/out/clojure/browser/repl.js:23:15
|
|
clojure$browser$repl$connect/</<@http://localhost:9000/out/clojure/browser/repl.js:121:128
|
|
goog.messaging.AbstractChannel.prototype.deliver@http://localhost:9000/out/goog/messaging/abstractchannel.js:142:5
|
|
goog.net.xpc.CrossPageChannel.prototype.xpcDeliver@http://localhost:9000/out/goog/net/xpc/crosspagechannel.js:733:7
|
|
goog.net.xpc.NativeMessagingTransport.messageReceived_@http://localhost:9000/out/goog/net/xpc/nativemessagingtransport.js:321:1
|
|
goog.events.fireListener@http://localhost:9000/out/goog/events/events.js:741:10
|
|
goog.events.handleBrowserEvent_@http://localhost:9000/out/goog/events/events.js:862:1
|
|
goog.events.getProxy/f<@http://localhost:9000/out/goog/events/events.js:276:16"
|
|
{:ua-product :firefox}
|
|
nil)
|
|
)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Rhino Stacktrace
|
|
|
|
(defmethod parse-stacktrace :rhino
|
|
[repl-env st err {:keys [output-dir] :as opts}]
|
|
(letfn [(process-frame [frame-str]
|
|
(when-not (or (string/blank? frame-str)
|
|
(== -1 (.indexOf frame-str "\tat")))
|
|
(let [[file-side line-fn-side] (string/split frame-str #":")
|
|
file (string/replace file-side #"\s+at\s+" "")
|
|
[line function] (string/split line-fn-side #"\s+")]
|
|
{:file (string/replace file
|
|
(str output-dir
|
|
#?(:clj File/separator :cljs "/"))
|
|
"")
|
|
:function (when function
|
|
(-> function
|
|
(string/replace "(" "")
|
|
(string/replace ")" "")))
|
|
:line (when (and line (not (string/blank? line)))
|
|
(parse-int line))
|
|
:column 0})))]
|
|
(->> (string/split st #"\n")
|
|
(map process-frame)
|
|
(remove nil?)
|
|
vec)))
|
|
|
|
(comment
|
|
(parse-stacktrace {}
|
|
"\tat .cljs_rhino_repl/goog/../cljs/core.js:4215 (seq)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:4245 (first)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:5295 (ffirst)
|
|
\tat <cljs repl>:1
|
|
\tat <cljs repl>:1"
|
|
{:ua-product :rhino}
|
|
{:output-dir ".cljs_rhino_repl"})
|
|
|
|
(parse-stacktrace {}
|
|
"org.mozilla.javascript.JavaScriptException: Error: 1 is not ISeqable (.cljs_rhino_repl/goog/../cljs/core.js#3998)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:3998 (cljs$core$seq)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:4017 (cljs$core$first)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:5160 (cljs$core$ffirst)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:16005
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:16004
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:10243
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:10334
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:3979 (cljs$core$seq)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28083 (cljs$core$pr_sequential_writer)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28811
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28267 (cljs$core$pr_writer_impl)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28349 (cljs$core$pr_writer)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28353 (cljs$core$pr_seq_writer)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28416 (cljs$core$pr_sb_with_opts)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28430 (cljs$core$pr_str_with_opts)
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28524
|
|
\tat .cljs_rhino_repl/goog/../cljs/core.js:28520 (cljs$core$pr_str)
|
|
at <cljs repl>:1
|
|
"
|
|
{:ua-product :rhino}
|
|
{:output-dir ".cljs_rhino_repl"})
|
|
)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Nashorn Stacktrace
|
|
|
|
(defmethod parse-stacktrace :nashorn
|
|
[repl-env st err {:keys [output-dir] :as opts}]
|
|
(letfn [(process-frame [frame-str]
|
|
(when-not (or (string/blank? frame-str)
|
|
(== -1 (.indexOf frame-str "\tat")))
|
|
(let [frame-str (string/replace frame-str #"\s+at\s+" "")
|
|
[function file-and-line] (string/split frame-str #"\s+")
|
|
[file-part line-part] (string/split file-and-line #":")]
|
|
{:file (string/replace (.substring file-part 1)
|
|
(str output-dir
|
|
#?(:clj File/separator :cljs "/"))
|
|
"")
|
|
:function function
|
|
:line (when (and line-part (not (string/blank? line-part)))
|
|
(parse-int
|
|
(.substring line-part 0
|
|
(dec (count line-part)))))
|
|
:column 0})))]
|
|
(->> (string/split st #"\n")
|
|
(map process-frame)
|
|
(remove nil?)
|
|
vec)))
|
|
|
|
(comment
|
|
(parse-stacktrace {}
|
|
"Error: 1 is not ISeqable
|
|
\tat cljs$core$seq (.cljs_nashorn_repl/goog/../cljs/core.js:3998)
|
|
\tat cljs$core$first (.cljs_nashorn_repl/goog/../cljs/core.js:4017)
|
|
\tat cljs$core$ffirst (.cljs_nashorn_repl/goog/../cljs/core.js:5160)
|
|
\tat <anonymous> (.cljs_nashorn_repl/goog/../cljs/core.js:16005)
|
|
\tat <anonymous> (.cljs_nashorn_repl/goog/../cljs/core.js:16004)
|
|
\tat sval (.cljs_nashorn_repl/goog/../cljs/core.js:10243)
|
|
\tat cljs$core$ISeqable$_seq$arity$1-6 (.cljs_nashorn_repl/goog/../cljs/core.js:10334)
|
|
\tat cljs$core$seq (.cljs_nashorn_repl/goog/../cljs/core.js:3979)
|
|
\tat cljs$core$pr_sequential_writer (.cljs_nashorn_repl/goog/../cljs/core.js:28083)
|
|
\tat cljs$core$IPrintWithWriter$_pr_writer$arity$3-5 (.cljs_nashorn_repl/goog/../cljs/core.js:28811)
|
|
\tat cljs$core$pr_writer_impl (.cljs_nashorn_repl/goog/../cljs/core.js:28267)
|
|
\tat cljs$core$pr_writer (.cljs_nashorn_repl/goog/../cljs/core.js:28349)
|
|
\tat cljs$core$pr_seq_writer (.cljs_nashorn_repl/goog/../cljs/core.js:28353)
|
|
\tat cljs$core$pr_sb_with_opts (.cljs_nashorn_repl/goog/../cljs/core.js:28416)
|
|
\tat cljs$core$pr_str_with_opts (.cljs_nashorn_repl/goog/../cljs/core.js:28430)
|
|
\tat cljs$core$IFn$_invoke$arity$variadic-71 (.cljs_nashorn_repl/goog/../cljs/core.js:28524)
|
|
\tat cljs$core$pr_str (.cljs_nashorn_repl/goog/../cljs/core.js:28520)
|
|
\tat <anonymous> (<eval>:1)
|
|
\tat <program> (<eval>:1)\n"
|
|
{:ua-product :nashorn}
|
|
{:output-dir ".cljs_nashorn_repl"})
|
|
)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Node.js Stacktrace
|
|
|
|
(defmethod parse-stacktrace :nodejs
|
|
[repl-env st err {:keys [output-dir] :as opts}]
|
|
(letfn [(parse-source-loc-info [x]
|
|
(when (and x (not (string/blank? x)))
|
|
(parse-int x)))
|
|
(process-frame [frame-str]
|
|
(when-not (or (string/blank? frame-str)
|
|
(nil? (re-find #"^\s+at" frame-str)))
|
|
(let [frame-str (string/replace frame-str #"\s+at\s+" "")]
|
|
(when-not (string/starts-with? frame-str "repl:")
|
|
(let [parts (string/split frame-str #"\s+")
|
|
[function file&line] (if (== 2 (count parts))
|
|
[(first parts)
|
|
(subs (second parts) 1
|
|
(dec (count (second parts))))]
|
|
[nil (first parts)])
|
|
[file-part line-part col-part] (string/split file&line #":")]
|
|
{:file (if function
|
|
(cond-> file-part
|
|
output-dir
|
|
(string/replace
|
|
(str output-dir
|
|
#?(:clj File/separator :cljs "/"))
|
|
""))
|
|
file-part)
|
|
:function function
|
|
:line (parse-source-loc-info line-part)
|
|
:column (parse-source-loc-info col-part)})))))]
|
|
(->> (string/split st #"\n")
|
|
(map process-frame)
|
|
(remove nil?)
|
|
vec)))
|
|
|
|
(comment
|
|
(parse-stacktrace {}
|
|
"Error: 1 is not ISeqable
|
|
at cljs$core$seq (.cljs_node_repl/cljs/core.cljs:1118:20)
|
|
at repl:1:65
|
|
at repl:9:4
|
|
at repl:17:3
|
|
at repl:22:4
|
|
at Object.exports.runInThisContext (vm.js:54:17)
|
|
at Domain.<anonymous> ([stdin]:41:34)
|
|
at Domain.run (domain.js:228:14)
|
|
at Socket.<anonymous> ([stdin]:40:25)
|
|
at emitOne (events.js:77:13)"
|
|
|
|
{:ua-product :nodejs}
|
|
{:output-dir ".cljs_node_repl"})
|
|
)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; Stacktrace Mapping
|
|
|
|
(defn remove-ext [file]
|
|
(-> file
|
|
(string/replace #"\.js$" "")
|
|
(string/replace #"\.cljs$" "")
|
|
(string/replace #"\.cljc$" "")
|
|
(string/replace #"\.clj$" "")))
|
|
|
|
(defn mapped-line-column-call
|
|
"Given a cljs.source-map source map data structure map a generated line
|
|
and column back to the original line, column, and function called."
|
|
[sms file line column]
|
|
(let [source-map (get sms (symbol (string/replace (remove-ext file) "/" ".")))]
|
|
;; source maps are 0 indexed for columns
|
|
;; multiple segments may exist at column
|
|
;; the last segment seems most accurate
|
|
(letfn [(get-best-column [columns column]
|
|
(last (or (get columns
|
|
(last (filter #(<= % (dec column))
|
|
(sort (keys columns)))))
|
|
(second (first columns)))))
|
|
(adjust [mapped]
|
|
(vec (map #(%1 %2) [inc inc identity] mapped)))]
|
|
(let [default [line column nil]]
|
|
;; source maps are 0 indexed for lines
|
|
(if-let [columns (get source-map (dec line))]
|
|
(adjust (map (get-best-column columns column) [:line :col :name]))
|
|
default)))))
|
|
|
|
(defn mapped-frame
|
|
"Given opts and a canonicalized JavaScript stacktrace frame, return the
|
|
ClojureScript frame."
|
|
[{:keys [function file line column]} sms opts]
|
|
(let [no-source-file? (if-not file true (starts-with? file "<"))
|
|
[line' column' call] (if no-source-file?
|
|
[line column nil]
|
|
(mapped-line-column-call sms file line column))
|
|
file' (when-not no-source-file?
|
|
(if (ends-with? file ".js")
|
|
(str (subs file 0 (- (count file) 3)) ".cljs")
|
|
file))]
|
|
{:function function
|
|
:call call
|
|
:file (if no-source-file?
|
|
(str "NO_SOURCE_FILE" (when file (str " " file)))
|
|
file')
|
|
:line line'
|
|
:column column'}))
|
|
|
|
(defn mapped-stacktrace
|
|
"Given a vector representing the canonicalized JavaScript stacktrace
|
|
return the ClojureScript stacktrace. The canonical stacktrace must be
|
|
in the form:
|
|
|
|
[{:file <string>
|
|
:function <string>
|
|
:line <integer>
|
|
:column <integer>}*]
|
|
|
|
:file must be a URL path (without protocol) relative to :output-dir or a
|
|
identifier delimited by angle brackets. The returned mapped stacktrace will
|
|
also contain :url entries to the original sources if it can be determined
|
|
from the classpath."
|
|
([stacktrace sms]
|
|
(mapped-stacktrace stacktrace sms nil))
|
|
([stacktrace sms opts]
|
|
(letfn [(call->function [x]
|
|
(if (:call x)
|
|
(hash-map :function (:call x))
|
|
{}))
|
|
(call-merge [function call]
|
|
(merge-with
|
|
(fn [munged-fn-name unmunged-call-name]
|
|
(if (= munged-fn-name
|
|
(string/replace (munge unmunged-call-name) "." "$"))
|
|
unmunged-call-name
|
|
munged-fn-name))
|
|
function call))]
|
|
(let [mapped-frames (map (memoize #(mapped-frame % sms opts)) stacktrace)]
|
|
;; take each non-nil :call and optionally merge it into :function one-level
|
|
;; up to avoid replacing with local symbols, we only replace munged name if
|
|
;; we can munge call symbol back to it
|
|
(vec (map call-merge
|
|
(map #(dissoc % :call) mapped-frames)
|
|
(concat (rest (map call->function mapped-frames)) [{}])))))))
|
|
|
|
(defn mapped-stacktrace-str
|
|
"Given a vector representing the canonicalized JavaScript stacktrace and a map
|
|
of library names to decoded source maps, print the ClojureScript stacktrace .
|
|
See mapped-stacktrace."
|
|
([stacktrace sms]
|
|
(mapped-stacktrace-str stacktrace sms nil))
|
|
([stacktrace sms opts]
|
|
(with-out-str
|
|
(doseq [{:keys [function file line column]}
|
|
(mapped-stacktrace stacktrace sms opts)]
|
|
(println "\t"
|
|
(str (when function (str function " "))
|
|
"(" file (when line (str ":" line))
|
|
(when column (str ":" column)) ")"))))))
|
|
|
|
(comment
|
|
(require '[cljs.closure :as cljsc]
|
|
'[clojure.data.json :as json]
|
|
'[cljs.source-map :as sm]
|
|
'[clojure.pprint :as pp])
|
|
|
|
(cljsc/build "samples/hello/src"
|
|
{:optimizations :none
|
|
:output-dir "samples/hello/out"
|
|
:output-to "samples/hello/out/hello.js"
|
|
:source-map true})
|
|
|
|
(def sms
|
|
{'hello.core
|
|
(sm/decode
|
|
(json/read-str
|
|
(slurp "samples/hello/out/hello/core.js.map")
|
|
:key-fn keyword))})
|
|
|
|
(pp/pprint sms)
|
|
|
|
;; maps to :line 5 :column 24
|
|
(mapped-stacktrace
|
|
[{:file "hello/core.js"
|
|
:function "first"
|
|
:line 6
|
|
:column 0}]
|
|
sms {:output-dir "samples/hello/out"})
|
|
|
|
(mapped-stacktrace-str
|
|
[{:file "hello/core.js"
|
|
:function "first"
|
|
:line 6
|
|
:column 0}]
|
|
sms {:output-dir "samples/hello/out"})
|
|
) |