Skip to content

Commit 2e864d0

Browse files
authored
Categorize more frames as :tooling (#11)
1 parent f15c0ac commit 2e864d0

File tree

10 files changed

+131
-54
lines changed

10 files changed

+131
-54
lines changed

.circleci/config.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defaults: &defaults
1616
executors:
1717
openjdk8:
1818
docker:
19-
- image: circleci/clojure:openjdk-8-lein-2.9.5
19+
- image: circleci/clojure:openjdk-8-lein-2.9.1-node
2020
environment:
2121
LEIN_ROOT: "true" # we intended to run lein as root
2222
JVM_OPTS: -Xmx3200m # limit the maximum heap size to prevent out of memory errors
@@ -32,15 +32,15 @@ executors:
3232

3333
openjdk11:
3434
docker:
35-
- image: circleci/clojure:openjdk-11-lein-2.9.5
35+
- image: circleci/clojure:openjdk-11-lein-2.9.3-buster-node
3636
environment:
3737
LEIN_ROOT: "true" # we intended to run lein as root
3838
JVM_OPTS: -Xmx3200m --illegal-access=deny # forbid reflective access (this flag doesn't exist for JDK8 or JDK17+)
3939
<<: *defaults
4040

4141
openjdk17:
4242
docker:
43-
- image: circleci/clojure:openjdk-17-lein-2.9.5-buster
43+
- image: circleci/clojure:openjdk-17-lein-2.9.5-buster-node
4444
environment:
4545
LEIN_ROOT: "true" # we intended to run lein as root
4646
JVM_OPTS: -Xmx3200m
@@ -153,8 +153,11 @@ jobs:
153153
cache_version: << parameters.clojure_version >>|<< parameters.jdk_version >>
154154
steps:
155155
- run:
156-
name: Running tests
156+
name: Running JVM tests
157157
command: make test
158+
- run:
159+
name: Running cljs tests
160+
command: make test-cljs
158161

159162
######################################################################
160163
#

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55
## Changes
66

77
* `analyzer`: include a `:phase` key for the causes that include a `:clojure.error/phase`.
8+
* Categorize more frames as `:tooling`
9+
* `:tooling` now intends to more broadly hide things that are commonly Clojure-internal / irrelevant to the application programmer.
10+
* New exhaustive list:
11+
* `cider.*`
12+
* `clojure.core/apply`
13+
* `clojure.core/binding-conveyor-fn`
14+
* `clojure.core/eval`
15+
* `clojure.core/with-bindings`
16+
* `clojure.lang.Compiler`
17+
* `clojure.lang.RT`
18+
* `clojure.main/repl`
19+
* `nrepl.*`
20+
* `java.lang.Thread/run` (if it's the root element of the stacktrace)
821

922
## 0.1.0
1023

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ clean:
88
test: clean
99
lein with-profile -user,-dev,+$(VERSION) test
1010

11-
test-cljs:
12-
lein cljsbuild once
11+
test-cljs: clean
12+
lein with-profile -user,-dev,+cljsbuild cljsbuild once
1313
node target/cljs/test.js
1414

1515
cljfmt:

README.org

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,6 @@ We get back a sequence of maps, one for each cause, which contain
186186
additional information about each frame discovered from the class path.
187187

188188
** Development
189-
*** Deployment
190-
191-
Here's how to deploy to Clojars:
192-
193-
#+begin_src sh
194-
git tag -a v0.1.0 -m "0.1.0"
195-
git push --tags
196-
#+end_src
197189

198190
*** Creating a parser
199191

@@ -255,6 +247,15 @@ for writing Instaparse grammars:
255247
- If your parser fails on an input, [[https://github.com/Engelberg/instaparse#revealing-hidden-information][reveal hidden information]] to get a
256248
better understanding of what happened.
257249

250+
*** Deployment
251+
252+
Here's how to deploy to Clojars:
253+
254+
#+begin_src sh
255+
git tag -a v0.1.0 -m "0.1.0"
256+
git push --tags
257+
#+end_src
258+
258259
** Changelog
259260

260261
[[CHANGELOG.md][CHANGELOG.md]]

project.clj

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
;; whenever we perform a deployment.
33
(defproject mx.cider/haystack (or (not-empty (System/getenv "PROJECT_VERSION"))
44
"0.0.0")
5-
:description ""
5+
:description "Let's make the most of Clojure's infamous stacktraces!"
66
:url "https://github.com/clojure-emacs/haystack"
77
:license {:name "Eclipse Public License"
88
:url "https://www.eclipse.org/legal/epl-v10.html"}
@@ -16,15 +16,7 @@
1616
:username :env/clojars_username
1717
:password :env/clojars_password
1818
:sign-releases false}]]
19-
:plugins [[lein-cljsbuild "1.1.8"]]
20-
:cljsbuild {:builds
21-
[{:id "test"
22-
:compiler
23-
{:main haystack.test.runner
24-
:output-dir "target/cljs/test"
25-
:output-to "target/cljs/test.js"
26-
:target :nodejs}
27-
:source-paths ["src" "test"]}]}
19+
2820
:profiles {:provided {:dependencies [[org.clojure/clojure "1.11.1"]
2921
[org.clojure/clojurescript "1.11.4"]]}
3022

@@ -40,7 +32,16 @@
4032
"https://oss.sonatype.org/content/repositories/snapshots"]]
4133
:dependencies [[org.clojure/clojure "1.12.0-master-SNAPSHOT"]
4234
[org.clojure/clojure "1.12.0-master-SNAPSHOT" :classifier "sources"]]}
43-
35+
:cljsbuild {:plugins [[lein-cljsbuild "1.1.8"]]
36+
:dependencies [[lein-doo "0.1.11"]]
37+
:cljsbuild {:builds
38+
[{:id "test"
39+
:compiler
40+
{:main haystack.test.runner
41+
:output-dir "target/cljs/test"
42+
:output-to "target/cljs/test.js"
43+
:target :nodejs}
44+
:source-paths ["src" "test"]}]}}
4445
:cljfmt [:test
4546
{:plugins [[lein-cljfmt "0.9.0" :exclusions [org.clojure/clojure
4647
org.clojure/clojurescript]]]}]

src/haystack/analyzer.clj

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
(or (info/file-path path) (second (resource/resource-path-tuple path))))
5555

5656
(defn- frame->url
57-
"Return a java.net.URL to the file referenced in the frame, if possible.
57+
"Return a `java.net.URL` to the file referenced in the frame, if possible.
5858
Useful for handling clojure vars which may not exist. Uncomprehensive list of
5959
reasons for this:
6060
* Failed refresh
@@ -120,17 +120,30 @@
120120
(flag-frame frame :repl)
121121
frame))
122122

123+
(def ^:private tooling-frame-re
124+
#"^clojure\.lang\.AFn|^clojure\.lang\.RestFn|^clojure\.lang\.RT|clojure\.lang\.Compiler|^nrepl\.|^cider\.|^clojure\.core/eval|^clojure\.core/apply|^clojure\.core/with-bindings|^clojure\.core/binding-conveyor-fn|^clojure\.main/repl")
125+
126+
(defn- tooling-frame-name? [frame-name last?]
127+
(let [demunged (repl/demunge frame-name)]
128+
(boolean (or (re-find tooling-frame-re demunged)
129+
(and last?
130+
;; Everything runs from a Thread, so this frame, if at root, is irrelevant.
131+
;; However one can invoke this method 'by hand', which is why we also observe `last?`.
132+
(re-find #"^java\.lang\.Thread/run" demunged))))))
133+
123134
(defn- flag-tooling
124-
"Walk the call stack from top to bottom, flagging frames below the first call
125-
to `clojure.lang.Compiler` or `nrepl.*` as `:tooling` to
126-
distinguish compilation and nREPL middleware frames from user code."
135+
"Given a collection of stack `frames`, marks the 'tooling' ones as such.
136+
137+
A 'tooling' frame is one that generally represents Clojure, JVM, nREPL or CIDER internals,
138+
and that is therefore not relevant to application-level code."
127139
[frames]
128-
(let [tool-regex #"^clojure\.lang\.Compiler|^nrepl\.|^cider\."
129-
tool? #(re-find tool-regex (or (:name %) ""))
130-
flag #(if (tool? %)
131-
(flag-frame % :tooling)
132-
%)]
133-
(map flag frames)))
140+
(let [last-index (dec (count frames))]
141+
(into []
142+
(map-indexed (fn [i {frame-name :name :as frame}]
143+
(cond-> frame
144+
(some-> frame-name (tooling-frame-name? (= i last-index)))
145+
(flag-frame :tooling))))
146+
frames)))
134147

135148
(defn directory-namespaces
136149
"Looks for all namespaces inside of directories on the class
@@ -326,7 +339,7 @@
326339
(flag-tooling)))))
327340

328341
(defn- analyze-cause
329-
"Analyze the `cause-data` of an exception in `Throwable->map` format."
342+
"Analyze the `cause-data` of an exception, in `Throwable->map` format."
330343
[cause-data print-fn]
331344
(let [pprint-str #(let [writer (StringWriter.)]
332345
(print-fn % writer)
@@ -368,8 +381,8 @@
368381
"Return the analyzed cause chain for `exception` beginning with the
369382
thrown exception. `exception` can be an instance of `Throwable` or a
370383
map in the same format as `Throwable->map`. For `ex-info`
371-
exceptions, the response contains a :data slot with the pretty
372-
printed data. For clojure.spec asserts, the :spec slot contains a
384+
exceptions, the response contains a `:data` slot with the pretty
385+
printed data. For clojure.spec asserts, the `:spec` slot contains a
373386
map of pretty printed components describing spec failures."
374387
{:added "0.1.0"}
375388
([exception]

test/haystack/analyzer_test.clj

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@
341341
:class "clojure.lang.AFn"
342342
:method "applyToHelper"
343343
:type :java
344-
:flags #{:java}}
344+
:flags #{:java :tooling}}
345345
(dissoc (first stacktrace) :file-url))))
346346
(testing "last frame"
347347
(is (= {:name "java.lang.Thread/run"
@@ -350,7 +350,7 @@
350350
:class "java.lang.Thread"
351351
:method "run"
352352
:type :java
353-
:flags #{:java}}
353+
:flags #{:java :tooling}}
354354
(dissoc (last stacktrace) :file-url)))))))
355355
(testing "second cause"
356356
(let [{:keys [class data message stacktrace]} (second causes)]
@@ -369,7 +369,7 @@
369369
:class "clojure.lang.AFn"
370370
:method "applyToHelper"
371371
:type :java
372-
:flags #{:java}}
372+
:flags #{:java :tooling}}
373373
(dissoc (first stacktrace) :file-url)))))))
374374
(testing "third cause"
375375
(let [{:keys [class data message stacktrace]} (nth causes 2)]
@@ -388,7 +388,7 @@
388388
:class "clojure.lang.AFn"
389389
:method "applyToHelper"
390390
:type :java
391-
:flags #{:java}}
391+
:flags #{:java :tooling}}
392392
(dissoc (first stacktrace) :file-url)))))))))
393393

394394
(deftest test-analyze-short-clojure-tagged-literal-println
@@ -411,7 +411,7 @@
411411
:class "java.lang.Thread"
412412
:method "run"
413413
:type :java
414-
:flags #{:java}}
414+
:flags #{:java :tooling}}
415415
(dissoc (first stacktrace) :file-url)))))))))
416416

417417
(deftest test-analyze-java
@@ -434,7 +434,7 @@
434434
:class "clojure.lang.AFn"
435435
:method "applyToHelper"
436436
:type :java
437-
:flags #{:java}}
437+
:flags #{:java :tooling}}
438438
(dissoc (first stacktrace) :file-url))))
439439
(testing "last frame"
440440
(is (= {:name "java.lang.Thread/run"
@@ -443,7 +443,7 @@
443443
:class "java.lang.Thread"
444444
:method "run"
445445
:type :java
446-
:flags #{:java}}
446+
:flags #{:java :tooling}}
447447
(dissoc (last stacktrace) :file-url)))))))
448448
(testing "second cause"
449449
(let [{:keys [class data message stacktrace]} (second causes)]
@@ -462,7 +462,7 @@
462462
:class "clojure.lang.AFn"
463463
:method "applyToHelper"
464464
:type :java
465-
:flags #{:java}}
465+
:flags #{:java :tooling}}
466466
(dissoc (first stacktrace) :file-url))))
467467
(testing "last frame"
468468
(is (= {:name "clojure.lang.Compiler$InvokeExpr/eval"
@@ -490,7 +490,7 @@
490490
:class "clojure.lang.AFn"
491491
:method "applyToHelper"
492492
:type :java
493-
:flags #{:java}}
493+
:flags #{:java :tooling}}
494494
(dissoc (first stacktrace) :file-url))))
495495
(testing "last frame"
496496
(is (= {:name "clojure.lang.Compiler$InvokeExpr/eval"
@@ -544,12 +544,12 @@
544544
:class "clojure.lang.AFn"
545545
:method "applyToHelper"
546546
:type :java
547-
:flags #{:java}}
547+
:flags #{:java :tooling}}
548548
(dissoc (nth stacktrace 0) :file-url))))
549549
(testing "2nd frame"
550550
(is (= {:class "clojure.lang.AFn"
551551
:file "AFn.java"
552-
:flags #{:java}
552+
:flags #{:java :tooling}
553553
:line 144
554554
:method "applyTo"
555555
:name "clojure.lang.AFn/applyTo"
@@ -572,7 +572,7 @@
572572
:line 160
573573
:method "applyToHelper"
574574
:type :java
575-
:flags #{:java}}
575+
:flags #{:java :tooling}}
576576
(dissoc (nth stacktrace 0) :file-url)))))))
577577
(testing "third cause"
578578
(let [{:keys [class data message stacktrace]} (nth causes 2 nil)]
@@ -591,7 +591,7 @@
591591
:line 156
592592
:method "applyToHelper"
593593
:type :java
594-
:flags #{:java}}
594+
:flags #{:java :tooling}}
595595
(dissoc (nth stacktrace 0) :file-url))))))))
596596

597597
(let [{:keys [major minor]} *clojure-version*]
@@ -604,3 +604,49 @@
604604
(catch Throwable e
605605
(sut/analyze e)))
606606
(map :phase))))))))
607+
608+
(deftest tooling-frame-name?
609+
(are [frame-name expected] (testing frame-name
610+
(is (= expected
611+
(#'sut/tooling-frame-name? frame-name false)))
612+
true)
613+
"cider.foo" true
614+
"acider.foo" false
615+
;; `+` is "application" level, should not be hidden:
616+
"clojure.core/+" false
617+
;; `apply` typically is internal, should be hidden:
618+
"clojure.core/apply" true
619+
"clojure.core/binding-conveyor-fn/fn" true
620+
"clojure.core/eval" true
621+
"clojure.core/with-bindings*" true
622+
"clojure.lang.AFn/applyTo" true
623+
"clojure.lang.AFn/applyToHelper" true
624+
"clojure.lang.RestFn/invoke" true
625+
"clojure.main/repl" true
626+
"clojure.main$repl$read_eval_print__9234$fn__9235/invoke" true
627+
"nrepl.foo" true
628+
"nrepl.middleware.interruptible_eval$evaluate/invokeStatic" true
629+
"anrepl.foo" false
630+
;; important case - `Numbers` is relevant, should not be hidden:
631+
"clojure.lang.Numbers/divide" false)
632+
633+
(is (not (#'sut/tooling-frame-name? "java.lang.Thread/run" false)))
634+
(is (#'sut/tooling-frame-name? "java.lang.Thread/run" true)))
635+
636+
(deftest flag-tooling
637+
(is (= [{:name "cider.foo", :flags #{:tooling}}
638+
{:name "java.lang.Thread/run"} ;; does not get the flag because it's not the root frame
639+
{:name "don't touch me 1"}
640+
{:name "nrepl.foo", :flags #{:tooling}}
641+
{:name "clojure.lang.RestFn/invoke", :flags #{:tooling}}
642+
{:name "don't touch me 2"}
643+
;; gets the flag because it's not the root frame:
644+
{:name "java.lang.Thread/run", :flags #{:tooling}}]
645+
(#'sut/flag-tooling [{:name "cider.foo"}
646+
{:name "java.lang.Thread/run"}
647+
{:name "don't touch me 1"}
648+
{:name "nrepl.foo"}
649+
{:name "clojure.lang.RestFn/invoke"}
650+
{:name "don't touch me 2"}
651+
{:name "java.lang.Thread/run"}]))
652+
"Adds the flag when appropiate, leaving other entries untouched"))

test/haystack/parser/clojure/repl_test.cljc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
:reason
114114
#?(:clj [{:tag :regexp :expecting "[a-zA-Z0-9_$/-]"}
115115
{:tag :regexp :expecting "[^\\S\\r\\n]+"}]
116-
:cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$\\/-]/"}
116+
:cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$/-]/"}
117117
{:tag :regexp, :expecting "/^[^\\S\\r\\n]+/"}])
118118
:line 1
119119
:column 1

test/haystack/parser/clojure/stacktrace_test.cljc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
:reason
116116
#?(:clj [{:tag :regexp :expecting "[a-zA-Z0-9_$/-]"}
117117
{:tag :regexp :expecting "[^\\S\\r\\n]+"}]
118-
:cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$\\/-]/"}
118+
:cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$/-]/"}
119119
{:tag :regexp, :expecting "/^[^\\S\\r\\n]+/"}])
120120
:line 1
121121
:column 1

0 commit comments

Comments
 (0)