Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: clojure-emacs/cider-nrepl
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: yuhan0/cider-nrepl
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: debug-exn
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
  • 14 commits
  • 8 files changed
  • 1 contributor

Commits on Jun 14, 2023

  1. Copy the full SHA
    6bee102 View commit details
  2. Copy the full SHA
    53b2891 View commit details
  3. Copy the full SHA
    bd45364 View commit details
  4. Copy the full SHA
    70a39f6 View commit details

Commits on Jun 15, 2023

  1. Use heuristic for instrumenting top level of #dbg

    Using #dbg on a def/defn should skip the breakpoint since return value
    is uninteresting. Doing otherwise causes :continue to behave incorrectly
    on a top-level instrumented form, since the the toplevel breakpoint is
    registered as the first coor even if it never triggers.
    yuhan0 committed Jun 15, 2023
    Copy the full SHA
    f007e7c View commit details
  2. Copy the full SHA
    2024bf4 View commit details
  3. Copy the full SHA
    297b2cc View commit details
  4. Copy the full SHA
    df2f6ca View commit details
  5. Fix some typos

    yuhan0 committed Jun 15, 2023
    Copy the full SHA
    e08bdb3 View commit details
  6. Update readme

    yuhan0 committed Jun 15, 2023
    Copy the full SHA
    2de1f77 View commit details
  7. Fix interop test failure on newer JDKs

    Test for elements in a known set instead of strict sequence equality,
    since the newly introduced (?) java.lang.StackTraceElement also has a
    .length method.
    yuhan0 committed Jun 15, 2023
    Copy the full SHA
    e381757 View commit details
  8. Fix content-type test failure on macOS

    Compare the canonical file paths, since Java creates temp files in /var
    which is a symlink for /private/var.
    yuhan0 committed Jun 15, 2023
    Copy the full SHA
    aaac6a9 View commit details
  9. Copy the full SHA
    40f1aac View commit details
  10. Fix indentation

    yuhan0 committed Jun 15, 2023
    Copy the full SHA
    705c3a9 View commit details
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -6,6 +6,9 @@

* Bump `cljfmt` to 0.9.2.
* Bump `puget` to 1.3.4.
* [#769](https://github.com/clojure-emacs/cider-nrepl/issues/769): Introduce new `#break!` and `#dbg!` reader macros for breaking on exception
* [#772](https://github.com/clojure-emacs/cider-nrepl/issues/772): Improve debugger heuristics on "interesting" breakpoints (Always break on forms tagged with #break, including vectors and maps)
* The `wrap-debug` middleware now adds an optional `:caught-msg` key to the `eval` op response, containing the exception message when caught by the debugger.

## 0.30.0 (2023-01-31)

4 changes: 2 additions & 2 deletions doc/modules/ROOT/pages/hacking.adoc
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
= Hacking on cider-nrepl

Hacking on cider-nrepl requires nothing bit a bit of knowledge of Clojure and nREPL.
Hacking on cider-nrepl requires nothing but a bit of knowledge of Clojure and nREPL.
In this section we'll take a look at some practical tips to make you more productive
while working on the project.

== Makefile

cider-nrepl has some pretty complicated Lein profiles, as it has to deal with multiple version of
cider-nrepl has some pretty complicated Lein profiles, as it has to deal with multiple versions of
Clojure and ClojureScript, plus dependency inlining with Mr. Anderson. That's why we've
added a good old `Makefile` to save you the trouble of having to think about the profiles
and just focus on the tasks at hand.
102 changes: 58 additions & 44 deletions src/cider/nrepl/middleware/debug.clj
Original file line number Diff line number Diff line change
@@ -284,15 +284,18 @@ this map (identified by a key), and will `dissoc` it afterwards."}
:rendered pr-str)))

(defn- debug-stacktrace
"Create a dummy exception, send its stack."
[]
"Send the stacktrace of `value` if it is an exception.
Otherwise, create a dummy exception to view the call stack at the current location."
[value]
(debugger-send
{:status :stack
:causes [{:class "StackTrace"
:message "Harmless user-requested stacktrace"
:stacktrace (-> (Exception. "Dummy")
(stacktrace.analyzer/analyze (::print/print-fn *msg*))
last :stacktrace)}]}))
:causes (if (instance? Throwable value)
(stacktrace.analyzer/analyze value (::print/print-fn *msg*))
[{:class "StackTrace"
:message "Harmless user-requested stacktrace"
:stacktrace (-> (Exception. "Dummy")
(stacktrace.analyzer/analyze (::print/print-fn *msg*))
last :stacktrace)}])}))

(def debug-commands
"An unsorted set of commands supported by the debugger."
@@ -365,7 +368,7 @@ this map (identified by a key), and will `dissoc` it afterwards."}
value)
:here (do (skip-breaks! :before coord (:code dbg-state) force?)
value)
:stacktrace (do (debug-stacktrace)
:stacktrace (do (debug-stacktrace value)
(recur value dbg-state))
:trace (do (skip-breaks! :trace)
value)
@@ -488,6 +491,13 @@ this map (identified by a key), and will `dissoc` it afterwards."}
~@body))

(defmacro breakpoint-with-initial-debug-bindings
{:style/indent 1}
[form dbg-state original-form]
`(with-initial-debug-bindings
(breakpoint
~form ~dbg-state ~original-form)))

(defmacro breakpoint-if-interesting-with-initial-debug-bindings
{:style/indent 1}
[form dbg-state original-form]
`(with-initial-debug-bindings
@@ -568,68 +578,68 @@ this map (identified by a key), and will `dissoc` it afterwards."}
(and (seq? form)
(irrelevant-return-value-forms (first form)))))

(defmacro breakpoint
"Wrap form in a breakpoint unconditionally."
[form {:keys [coor] :as dbg-state} original-form]
(let [condition (:break/when (meta form))]
(if condition
;; If there is a condition and it is falsy, we need to skip
;; the current level (:deeper than parent coor), but only
;; once. Next time, we need to test the condition again.
`(let [old-breaks# @*skip-breaks*]
(when-not ~condition
(skip-breaks! :deeper ~(vec (butlast coor)) (:code (:msg ~'STATE__)) false))
(try
(expand-break ~form ~dbg-state ~original-form)
;; in case :continue-all was requested in a deeper level
;; we don't want go back to the old-breaks
(finally (when (not= :all (:mode @*skip-breaks*))
(reset! *skip-breaks* old-breaks#)))))
`(expand-break ~form ~dbg-state ~original-form))))

(defmacro breakpoint-if-interesting
"Wrap form in a breakpoint if it looks interesting.
Uninteresting forms are symbols that resolve to `clojure.core`
(taking locals into account), and sexps whose head is present in
`irrelevant-return-value-forms`. Used as :breakfunction in `tag-form`."
[form {:keys [coor] :as dbg-state} original-form]
[form dbg-state original-form]
(if (uninteresting-form? &env form)
form
(let [condition (:break/when (meta form))]
(if condition
;; If there is a condition and it is falsy, we need to skip
;; the current level (:deeper than parent coor), but only
;; once. Next time, we need to test the condition again.
`(let [old-breaks# @*skip-breaks*]
(when-not ~condition
(skip-breaks! :deeper ~(vec (butlast coor)) (:code (:msg ~'STATE__)) false))
(try
(expand-break ~form ~dbg-state ~original-form)
;; in case :continue-all was requested in a deeper level
;; we don't want go back to the old-breaks
(finally (when (not= :all (:mode @*skip-breaks*))
(reset! *skip-breaks* old-breaks#)))))
`(expand-break ~form ~dbg-state ~original-form)))))
`(breakpoint ~form ~dbg-state ~original-form)))

(defmacro breakpoint-if-exception
"Wrap form in a try-catch that has breakpoint on exception.
Ignores uninteresting forms.
Uninteresting forms are symbols that resolve to `clojure.core`
(taking locals into account), and sexps whose head is present in
`irrelevant-return-value-forms`. Used as :breakfunction in `tag-form`."
Used as :breakfunction in `tag-form`."
[form dbg-state original-form]
(if (uninteresting-form? &env form)
form
`(try ~form
(catch Throwable ex#
(let [exn-message# (.getMessage ex#)
break-result# (expand-break exn-message# ~dbg-state ~original-form)]
(if (= exn-message# break-result#)
;; if they continued then rethrow the exception
(throw ex#)
;; otherwise return the value they injected
break-result#))))))
`(try ~form
(catch Throwable ex#
(let [~'STATE__ (assoc-in ~'STATE__ [:msg :caught-msg] (.getMessage ex#))
break-result# (expand-break ex# ~dbg-state ~original-form)]
(if (= ex# break-result#)
;; if they continued then rethrow the exception
(throw ex#)
;; otherwise return the value they injected
break-result#)))))

;;; ## Data readers
;;
;; Set in `src/data_readers.clj`.
(defn breakpoint-reader
"#break reader. Mark `form` for breakpointing."
[form]
(ins/tag-form form #'breakpoint-with-initial-debug-bindings))
(ins/tag-form form #'breakpoint-with-initial-debug-bindings true))

(defn debug-reader
"#dbg reader. Mark all forms in `form` for breakpointing.
`form` itself is also marked."
[form]
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-interesting)
#'breakpoint-with-initial-debug-bindings))
#'breakpoint-if-interesting-with-initial-debug-bindings))

(defn break-on-exception-reader
"#exn reader. Wrap `form` in try-catch and break only on exception"
[form]
(ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings))
(ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings true))

(defn debug-on-exception-reader
"#dbgexn reader. Mark all forms in `form` for breakpointing on exception.
@@ -655,6 +665,10 @@ this map (identified by a key), and will `dissoc` it afterwards."}
(eval form1)))
(throw e))))))

(def ^:dynamic *debug-data-readers*
"Reader macros like #dbg which cause code to be instrumented when present."
'#{dbg exn dbgexn break light})

;;; ## Middleware
(defn- maybe-debug
"Return msg, prepared for debugging if code contains debugging macros."
@@ -666,8 +680,8 @@ this map (identified by a key), and will `dissoc` it afterwards."}
fake-reader (fn [x] (reset! has-debug? true) x)]
(binding [*ns* (find-ns (symbol (or ns "user")))
*data-readers* (->> (repeat fake-reader)
(interleave '[dbg exn dbgexn break light])
(apply assoc *data-readers*))]
(zipmap *debug-data-readers*)
(merge *data-readers*))]
(try
;; new-line in REPL always throws; skip for debug convenience
(when (> (count code) 3)
17 changes: 12 additions & 5 deletions src/cider/nrepl/middleware/util/instrument.clj
Original file line number Diff line number Diff line change
@@ -225,9 +225,11 @@
;; Other coll types are safe, so we go inside them and only
;; instrument what's interesting.
;; Do we also need to check for seq?
coll? (doall (instrument-coll form))
coll? (cond-> (doall (instrument-coll form))
(::do-break (meta form)) (with-break))
;; Other things are uninteresting, literals or unreadable objects.
form))
(cond-> form
(::do-break (meta form)) (with-break))))

;;;; ## Pre-instrumentation
;;;
@@ -294,9 +296,14 @@
"Tag form to be instrumented with breakfunction.
This sets the ::breakfunction metadata of form, which can then be
used by `instrument-tagged-code`. See this function for the meaning
of breakfunction."
[form breakfunction]
(m/merge-meta form {::breakfunction breakfunction}))
of breakfunction.
When `do-break?` is true it tells the instrumenter to wrap the form
with a breakpoint regardless of other heuristics."
([form breakfunction]
(tag-form form breakfunction false))
([form breakfunction do-break?]
(m/merge-meta form {::breakfunction breakfunction}
(when do-break? {::do-break true}))))

(defn tag-form-recursively
"Like `tag-form` but also tag all forms inside the given form."
4 changes: 2 additions & 2 deletions src/data_readers.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{dbg cider.nrepl.middleware.debug/debug-reader
break cider.nrepl.middleware.debug/breakpoint-reader
exn cider.nrepl.middleware.debug/break-on-exception-reader
dbgexn cider.nrepl.middleware.debug/debug-on-exception-reader
break! cider.nrepl.middleware.debug/break-on-exception-reader
dbg! cider.nrepl.middleware.debug/debug-on-exception-reader
light cider.nrepl.middleware.enlighten/light-reader}
7 changes: 4 additions & 3 deletions test/clj/cider/nrepl/middleware/content_type_test.clj
Original file line number Diff line number Diff line change
@@ -44,14 +44,15 @@
[:body :content-type :content-transfer-encoding :status]))))

(testing "java.io.File"
(let [f (java.io.File/createTempFile "foo" ".txt")]
(let [f (java.io.File/createTempFile "foo" ".txt")
path (.getCanonicalPath f)]
(is (= {:body ""
:content-type
["message/external-body"
{:URL (str "file:" f) :access-type "URL"}]
{:URL (str "file:" path) :access-type "URL"}]
:status #{"done"}}
(-> {:op "eval"
:code (str "(java.io.File. " (pr-str (str f)) ")")
:code (str "(java.io.File. " (pr-str path) ")")
:content-type "true"}
session/message
(select-keys [:body :content-type :content-transfer-encoding :status]))))))
47 changes: 44 additions & 3 deletions test/clj/cider/nrepl/middleware/debug_integration_test.clj
Original file line number Diff line number Diff line change
@@ -136,18 +136,59 @@
(f)))))

(deftest debug-expression-test
(--> :eval "(ns user.test.debug)")
(<-- {:ns "user.test.debug"})
(<-- {:status ["done"]})

(testing "normal eval (no debugging)"
(--> :eval "(+ 2 3)")
(<-- {:value "5"})
(<-- {:status ["done"]}))

(testing "#break reader, no breakpoints"
;; This code has only literals and core functions, so it should
;; not break, but should just return the value
(testing "Top level breakpoints do not trigger"
;; Since triggering a breakpoint to inspect the toplevel value is no different
;; from returning the value.
(--> :eval "#break (+ 2 3)")
(<-- {:value "5"})
(<-- {:status ["done"]}))

(testing "#break on return value of call"
(--> :eval "(do #break (+ 2 3) :ok)")
(<-- {:debug-value "5"})
(--> :next)
(<-- {:value ":ok"})
(<-- {:status ["done"]}))

(testing "#dbg reader on uninteresting forms"
;; The return value of a def form is uninteresting, as well as the sub forms
;; which are just core vars and literals.
;; #dbg should not break on any of them and just return the value.
(--> :eval "(do #dbg (def foo [2 + {:skip \"me\"}]) foo)")
(<-- {:value "[2 #function[clojure.core/+] {:skip \"me\"}]"})
(<-- {:status ["done"]}))

(testing "#break reader on uninteresting forms"
;; #break signifies that the user explicitly wants to set a breakpoint there,
;; so ignore any of #dbg reader's heuristics on what an interesting form is.
(--> :eval "(let [a 1] #break [2 3], #break {:a a}, #break + #break (def foo 4) foo) ")
(<-- {:debug-value "[2 3]"})
(--> :next)
(<-- {:debug-value "{:a 1}"})
(--> :next)
(<-- {:debug-value "#function[clojure.core/+]"})
(--> :next)
(<-- {:debug-value "#'user.test.debug/foo"})
(--> :next)
(<-- {:value "4"})
(<-- {:status ["done"]}))

(testing "#break reader on literals"
;; The instrumenter cannot tag literals which don't support metadata,
;; so #break does not work on them.
(--> :eval "(do #break :kwd, #break 123, #break \"string\" :ok)")
(<-- {:value ":ok"})
(<-- {:status ["done"]}))

(testing "#dbg reader, with breaks"
(--> :eval
"#dbg
4 changes: 2 additions & 2 deletions test/clj/cider/nrepl/middleware/info_test.clj
Original file line number Diff line number Diff line change
@@ -310,8 +310,8 @@

(testing "java interop method with multiple classes"
(let [response (session/message {:op "eldoc" :sym ".length" :ns "cider.nrepl.middleware.info-test"})]
(is (= (:class response)
["java.lang.String" "java.lang.StringBuffer" "java.lang.CharSequence" "java.lang.StringBuilder"])
(is (every? (set (:class response)) ;; is a superset of:
["java.lang.String" "java.lang.StringBuffer" "java.lang.CharSequence" "java.lang.StringBuilder"])
(pr-str response))
(is (= (:member response) "length"))
(is (not (contains? response :ns)))