Skip to content

Commit c1e67c2

Browse files
authored
Pass nREPL client evaled code's line and col nums to the reader (#1038)
Hi, could you please consider patch to pass the nREPL client's eval file line and column numbers down to the reader for more accurate compiler exception reporting. It fixes #1037. I have attempted a focused approach to propagate the line and column parameters to the error reporting mechanism. If this approach is not optimal, I am open to reorganizing the code based on better suggestions. I've included tests down the affected path to cover these changes. Thanks --------- Co-authored-by: ikappaki <[email protected]>
1 parent 73b8b83 commit c1e67c2

File tree

7 files changed

+211
-25
lines changed

7 files changed

+211
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* Fix a bug where `defrecord`/`deftype` constructors could not be used in the type's methods. (#1025)
1414
* Fix a bug where `keys` and `vals` would fail for records (#1030)
1515
* Fix a bug where operations on records created by `defrecord` failed for fields whose Python-safe names were mangled by the Python compiler (#1029)
16+
* Fix incorrect line numbers for compiler exceptions in nREPL when evaluating forms in loaded files (#1037)
1617

1718
## [v0.2.1]
1819
### Changed

src/basilisp/contrib/nrepl_server.lpy

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,11 @@
106106
"ns" ns})))
107107

108108
(defn- do-handle-eval
109-
"Evaluate the ``request`` ``code`` of ``file`` in the ``ns`` namespace according
110-
to the current state of the ``client*`` and sends its result with ``send-fn``.
109+
"Evaluate the ``request`` ``code`` of ``file`` in the ``ns`` namespace
110+
according to the current state of the ``client*`` and sends its
111+
result with ``send-fn``. If ``line`` and/or ``column`` are provided,
112+
they indicate the line and column numbers withing the ``file`` where
113+
``code`` is located.
111114

112115
The result sent is either the last evaluated value or exception, followed by
113116
the \"done\" status.
@@ -119,7 +122,7 @@
119122
It binds the ``*1``, ``*2``, ``*3`` and ``*e`` variables for evaluation from
120123
the corresponding ones found in ``client*``, and updates the latter according
121124
to the result."
122-
[{:keys [client* code ns file _column _line] :as request} send-fn]
125+
[{:keys [client* code ns file column line] :as request} send-fn]
123126
(let [{:keys [*1 *2 *3 *e eval-ns]} @client*
124127
out-stream (StreamOutFn #(send-fn request {"out" %}))
125128
ctx (basilisp.lang.compiler.CompilerContext. (or file "<nREPL Input>"))
@@ -134,7 +137,10 @@
134137
*e *e]
135138
(try
136139
(let [result (last
137-
(for [form (read-seq (io/StringIO code))]
140+
(for [form (read-seq (cond-> {}
141+
line (assoc :init-line line)
142+
column (assoc :init-column column))
143+
(io/StringIO code))]
138144
(basilisp.lang.compiler/compile-and-exec-form form
139145
ctx
140146
*ns*)))]

src/basilisp/core.lpy

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4474,7 +4474,16 @@
44744474
The stream must satisfy the interface of :external:py:class:`io.TextIOBase`\\, but
44754475
does not require any pushback capabilities. The default
44764476
``basilisp.lang.reader.StreamReader`` can wrap any object implementing ``TextIOBase``
4477-
and provide pushback capabilities."
4477+
and provide pushback capabilities.
4478+
4479+
``opts`` may include, among other things, the following keys:
4480+
4481+
:keyword ``:init-line``: optional initial line number for reader metadata;
4482+
helpful for contextualizing errors for chunks of code read from a
4483+
larger source document.
4484+
:keyword ``:init-column``: optional initial column number for reader metadata;
4485+
helpful for contextualizing errors for chunks of code read from a
4486+
larger source document."
44784487
([stream]
44794488
(read-seq {} stream))
44804489
([opts stream]
@@ -4486,7 +4495,10 @@
44864495
(= (:eof opts) :eofthrow)
44874496
(:features opts)
44884497
(not= :preserve (:read-cond opts))
4489-
*default-data-reader-fn*)))))
4498+
*default-data-reader-fn*
4499+
**
4500+
:init-line (:init-line opts)
4501+
:init-column (:init-column opts))))))
44904502

44914503
(defn read-string
44924504
"Read a string of Basilisp code.

src/basilisp/lang/reader.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,23 @@ class StreamReader:
223223

224224
__slots__ = ("_stream", "_pushback_depth", "_idx", "_buffer", "_line", "_col")
225225

226-
def __init__(self, stream: io.TextIOBase, pushback_depth: int = 5) -> None:
226+
def __init__(
227+
self,
228+
stream: io.TextIOBase,
229+
pushback_depth: int = 5,
230+
init_line: Optional[int] = None,
231+
init_column: Optional[int] = None,
232+
) -> None:
233+
"""`init_line` and `init_column` refer to where the `stream`
234+
starts in the broader context, defaulting to 1 and 0
235+
respectively if not provided."""
236+
init_line = init_line if init_line is not None else 1
237+
init_column = init_column if init_column is not None else 0
227238
self._stream = stream
228239
self._pushback_depth = pushback_depth
229240
self._idx = -2
230-
self._line = collections.deque([1], pushback_depth)
231-
self._col = collections.deque([0], pushback_depth)
241+
self._line = collections.deque([init_line], pushback_depth)
242+
self._col = collections.deque([init_column], pushback_depth)
232243
self._buffer = collections.deque([self._stream.read(1)], pushback_depth)
233244

234245
# Load up an extra character
@@ -1696,9 +1707,15 @@ def read( # pylint: disable=too-many-arguments
16961707
features: Optional[IPersistentSet[kw.Keyword]] = None,
16971708
process_reader_cond: bool = True,
16981709
default_data_reader_fn: Optional[DefaultDataReaderFn] = None,
1710+
init_line: Optional[int] = None,
1711+
init_column: Optional[int] = None,
16991712
) -> Iterable[RawReaderForm]:
17001713
"""Read the contents of a stream as a Lisp expression.
17011714
1715+
The optional `init_line` and `init_column` specify where the
1716+
`stream` location metadata starts in the broader context, if not
1717+
from the start.
1718+
17021719
Callers may optionally specify a namespace resolver, which will be used
17031720
to adjudicate the fully-qualified name of symbols appearing inside of
17041721
a syntax quote.
@@ -1717,7 +1734,7 @@ def read( # pylint: disable=too-many-arguments
17171734
are provided.
17181735
17191736
The caller is responsible for closing the input stream."""
1720-
reader = StreamReader(stream)
1737+
reader = StreamReader(stream, init_line=init_line, init_column=init_column)
17211738
ctx = ReaderContext(
17221739
reader,
17231740
resolver=resolver,
@@ -1752,9 +1769,15 @@ def read_str( # pylint: disable=too-many-arguments
17521769
features: Optional[IPersistentSet[kw.Keyword]] = None,
17531770
process_reader_cond: bool = True,
17541771
default_data_reader_fn: Optional[DefaultDataReaderFn] = None,
1772+
init_line: Optional[int] = None,
1773+
init_column: Optional[int] = None,
17551774
) -> Iterable[RawReaderForm]:
17561775
"""Read the contents of a string as a Lisp expression.
17571776
1777+
The optional `init_line` and `init_column` specify where the
1778+
`stream` location metadata starts in the broader context, if not
1779+
from the beginning.
1780+
17581781
Keyword arguments to this function have the same meanings as those of
17591782
basilisp.lang.reader.read."""
17601783
with io.StringIO(s) as buf:
@@ -1767,6 +1790,8 @@ def read_str( # pylint: disable=too-many-arguments
17671790
features=features,
17681791
process_reader_cond=process_reader_cond,
17691792
default_data_reader_fn=default_data_reader_fn,
1793+
init_line=init_line,
1794+
init_column=init_column,
17701795
)
17711796

17721797

tests/basilisp/contrib/nrepl_server_test.lpy

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
[basilisp.contrib.nrepl-server :as nr]
66
[basilisp.set :as set]
77
[basilisp.string :as str :refer [starts-with?]]
8-
[basilisp.test :refer [deftest are is testing]])
8+
[basilisp.test :refer [deftest are is testing use-fixtures]]
9+
[basilisp.test.fixtures :as fixtures :refer [*tempdir*]])
910
(:import
1011
os
1112
socket
1213
tempfile
1314
threading
1415
time))
1516

17+
(use-fixtures :each fixtures/tempdir)
18+
1619
(def ^:dynamic *nrepl-port*
1720
"The port the :lpy:py:`with-server` is bound to."
1821
nil)
@@ -518,16 +521,29 @@
518521

519522
(deftest nrepl-server-load-file
520523
(testing "basic"
524+
521525
(with-server {}
522526
(with-connect client
523527
(let [id* (atom 0)
524-
id-inc! #(swap! id* inc)]
528+
id-inc! #(swap! id* inc)
529+
filename (str (bio/path *tempdir* "load-file-test.lpy"))]
525530
(client-send! client {:id (id-inc!) :op "clone"})
526531
(let [{:keys [status]} (client-recv! client)]
527532
(is (= ["done"] status)))
533+
534+
(spit filename "(ns abc.xyz (:require [clojure.string :as str])
535+
(:import [sys :as s]))
536+
(defn afn []
537+
(str/lower-case \"ABC\"))
538+
(comment
539+
(abc)
540+
(xyz))
541+
542+
(afn)")
543+
528544
(client-send! client {:id (id-inc!) :op "load-file"
529-
:ns "user" :file "(ns abc.xyz (:require [clojure.string :as str]) (:import [sys :as s])) (defn afn [] (str/lower-case \"ABC\")) (afn)"
530-
:file-name "xyz.lpy" :file-path "/abc/xyz.lpy"})
545+
:ns "user" :file (slurp filename)
546+
:file-name "xyz.lpy" :file-path filename})
531547
(are [response] (= response (client-recv! client))
532548
{:id @id* :ns "abc.xyz" :value "\"abc\""}
533549
{:id @id* :ns "abc.xyz" :status ["done"]})
@@ -559,6 +575,40 @@
559575
{:id @id* :ns "abc.other" :value "\"abc\""}
560576
{:id @id* :ns "abc.other" :status ["done"]})
561577

578+
(client-send! client {:id (id-inc!) :ns "abc.xyz" :op "eval" :code "(abc)"
579+
:file filename
580+
:line 6 :column 2})
581+
(let [{:keys [id err]} (client-recv! client)]
582+
(is (= @id* id))
583+
(is (= [""
584+
" exception: <class 'basilisp.lang.compiler.exception.CompilerException'>"
585+
" phase: :analyzing"
586+
" message: unable to resolve symbol 'abc' in this context"
587+
" form: abc"
588+
(str " location: " filename ":6")
589+
" context:"
590+
""
591+
" 2 | (:import [sys :as s]))"
592+
" 3 | (defn afn []"
593+
" 4 | (str/lower-case \"ABC\"))"
594+
" 5 | (comment"
595+
" 6 > | (abc)"
596+
" 7 | (xyz))"
597+
" 8 | "
598+
" 9 | (afn)"]
599+
(-> err
600+
;; remove ansi control chars
601+
(str/replace #"\\x1B[@-_][0-?]*[ -/]*[@-~]" "")
602+
(str/split-lines)))))
603+
(let [{:keys [id ex status ns]} (client-recv! client)]
604+
(is (= @id* id))
605+
(is (= "abc.xyz" ns))
606+
(is (= ["eval-error"] status))
607+
(is (str/starts-with? ex "Traceback (most recent call last):")))
608+
(are [response] (= response (client-recv! client))
609+
{:id @id* :ns "abc.xyz" :status ["done"]})
610+
611+
562612
(client-send! client {:id (id-inc!) :op "load-file" :ns "user" :file "(ns abc.third)\n\n(/ 3 0)"
563613
:file-name "third.lpy" :file-path "/abc/third.lpy"})
564614
(let [{:keys [id err]} (client-recv! client)]
@@ -571,18 +621,18 @@
571621
(is (not= -1 (.find ex "File \"/abc/third.lpy\", line 3")) ex)
572622
(is (str/starts-with? ex "Traceback (most recent call last):")))
573623
(are [response] (= response (client-recv! client))
574-
{:id @id* :ns "abc.third" :status ["done"]}))))
624+
{:id @id* :ns "abc.third" :status ["done"]})))))
575625

576-
(testing "no file"
577-
(with-server {}
578-
(with-connect client
579-
(client-send! client {:id 1 :op "clone"})
580-
(let [{:keys [status]} (client-recv! client)]
581-
(is (= ["done"] status)))
582-
(client-send! client {:id 2 :op "load-file" :ns "user"})
583-
(are [response] (= response (client-recv! client))
584-
{:id 2 :ns "user" :value "nil"}
585-
{:id 2 :ns "user" :status ["done"]}))))))
626+
(testing "no file"
627+
(with-server {}
628+
(with-connect client
629+
(client-send! client {:id 1 :op "clone"})
630+
(let [{:keys [status]} (client-recv! client)]
631+
(is (= ["done"] status)))
632+
(client-send! client {:id 2 :op "load-file" :ns "user"})
633+
(are [response] (= response (client-recv! client))
634+
{:id 2 :ns "user" :value "nil"}
635+
{:id 2 :ns "user" :status ["done"]})))))
586636

587637
(deftest nrepl-server-params
588638
(testing "buffer size"

tests/basilisp/reader_test.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,40 @@ def test_stream_reader_loc():
134134
assert (3, 1) == sreader.loc
135135

136136

137+
def test_stream_reader_loc_other():
138+
s = str("i=1\n" "b=2\n" "i")
139+
sreader = reader.StreamReader(io.StringIO(s), init_line=5, init_column=3)
140+
assert (5, 3) == sreader.loc
141+
142+
assert "i" == sreader.peek()
143+
assert (5, 3) == sreader.loc
144+
145+
assert "=" == sreader.next_char()
146+
assert (5, 4) == sreader.loc
147+
148+
assert "=" == sreader.peek()
149+
assert (5, 4) == sreader.loc
150+
151+
sreader.pushback()
152+
assert "i" == sreader.peek()
153+
assert (5, 3) == sreader.loc
154+
155+
assert "=" == sreader.next_char()
156+
assert (5, 4) == sreader.loc
157+
158+
assert "1" == sreader.next_char()
159+
assert (5, 5) == sreader.loc
160+
161+
assert "\n" == sreader.next_char()
162+
assert (5, 6) == sreader.loc
163+
164+
assert "b" == sreader.next_char()
165+
assert (6, 0) == sreader.loc
166+
167+
assert "=" == sreader.next_char()
168+
assert (6, 1) == sreader.loc
169+
170+
137171
class TestReaderLines:
138172
def test_reader_lines_from_str(self, tmp_path):
139173
_, _, l = list(reader.read_str("1\n2\n(/ 5 0)"))
@@ -145,6 +179,25 @@ def test_reader_lines_from_str(self, tmp_path):
145179
l.meta.get(reader.READER_END_COL_KW),
146180
)
147181

182+
def test_reader_lines_from_str_other_loc(self, tmp_path):
183+
l1, _, l3 = list(
184+
reader.read_str("(+ 1 2)\n2\n(/ 5 0)", init_line=6, init_column=7)
185+
)
186+
187+
assert (6, 6, 7, 14) == (
188+
l1.meta.get(reader.READER_LINE_KW),
189+
l1.meta.get(reader.READER_END_LINE_KW),
190+
l1.meta.get(reader.READER_COL_KW),
191+
l1.meta.get(reader.READER_END_COL_KW),
192+
)
193+
194+
assert (8, 8, 0, 7) == (
195+
l3.meta.get(reader.READER_LINE_KW),
196+
l3.meta.get(reader.READER_END_LINE_KW),
197+
l3.meta.get(reader.READER_COL_KW),
198+
l3.meta.get(reader.READER_END_COL_KW),
199+
)
200+
148201
def test_reader_lines_from_file(self, tmp_path):
149202
filename = tmp_path / "test.lpy"
150203

tests/basilisp/test_core_fns.lpy

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,6 +2438,45 @@
24382438
(is (= "Thu Jan 1 00:00:01 1970" (eval '(time-alias/asctime (time-alias/gmtime 1))))))))
24392439

24402440

2441+
;;;;;;;;;;
2442+
;; Read ;;
2443+
;;;;;;;;;;
2444+
2445+
(deftest read-seq-test
2446+
2447+
(testing "read-seq plain"
2448+
(with [stream (io/StringIO "(+ 1 2)\n(+ 4 \n5)")]
2449+
(let [[l1 l2] (read-seq stream)
2450+
{l1-l :basilisp.lang.reader/line
2451+
l1-el :basilisp.lang.reader/end-line
2452+
l1-c :basilisp.lang.reader/col
2453+
l1-ec :basilisp.lang.reader/end-col} (meta l1)
2454+
{l2-l :basilisp.lang.reader/line
2455+
l2-el :basilisp.lang.reader/end-line
2456+
l2-c :basilisp.lang.reader/col
2457+
l2-ec :basilisp.lang.reader/end-col} (meta l2)]
2458+
(is (= '(+ 1 2) l1))
2459+
(is (= '(+ 4 5) l2))
2460+
(is (= [1 1 0 7] [l1-l l1-el l1-c l1-ec]))
2461+
(is (= [2 3 0 2] [l2-l l2-el l2-c l2-ec])))))
2462+
2463+
(testing "read-seq with specified line and col numbers"
2464+
(with [stream (io/StringIO "(+ 1 2)\n(+ 4 \n5)")]
2465+
(let [[l1 l2] (read-seq {:init-line 10 :init-column 15} stream)
2466+
{l1-l :basilisp.lang.reader/line
2467+
l1-el :basilisp.lang.reader/end-line
2468+
l1-c :basilisp.lang.reader/col
2469+
l1-ec :basilisp.lang.reader/end-col} (meta l1)
2470+
{l2-l :basilisp.lang.reader/line
2471+
l2-el :basilisp.lang.reader/end-line
2472+
l2-c :basilisp.lang.reader/col
2473+
l2-ec :basilisp.lang.reader/end-col} (meta l2)]
2474+
(is (= '(+ 1 2) l1))
2475+
(is (= '(+ 4 5) l2))
2476+
(is (= [10 10 15 22] [l1-l l1-el l1-c l1-ec]))
2477+
(is (= [11 12 0 2] [l2-l l2-el l2-c l2-ec]))))))
2478+
2479+
24412480
;;;;;;;;;;
24422481
;; Load ;;
24432482
;;;;;;;;;;

0 commit comments

Comments
 (0)