Skip to content

Commit f83b0fe

Browse files
authored
Fix reader column offset numbering (#906)
Fixes #905
1 parent adaefb3 commit f83b0fe

File tree

7 files changed

+144
-58
lines changed

7 files changed

+144
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
* Fix a bug that caused a syntax error when presenting any filepath that includes the MS-Windows `\` file separator to the cli run command (#912)
1919
* Fix a bug where the core functions `symbol` and `keyword` would not accept non-string data types (#911)
2020
* Fix a bug where the compiler would emit warnings on when a Var was redef'ed even if that Var was initially defined with `^:redef` metadata (#916)
21+
* Fix a bug where reader column offset numbering began at 1, rather than 0 (#905)
2122

2223
### Other
2324
* Update Sphinx documentation theme (#909)

src/basilisp/lang/compiler/generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,12 +581,12 @@ def _ast_with_loc(
581581
dep.end_lineno = env.end_line
582582

583583
if env.col is not None and env.end_col is not None:
584-
py_ast.node.col_offset = env.col - 1
585-
py_ast.node.end_col_offset = env.end_col - 1
584+
py_ast.node.col_offset = env.col
585+
py_ast.node.end_col_offset = env.end_col
586586
if include_dependencies:
587587
for dep in py_ast.dependencies:
588-
dep.col_offset = env.col - 1
589-
dep.end_col_offset = env.end_col - 1
588+
dep.col_offset = env.col
589+
dep.end_col_offset = env.end_col
590590

591591
return py_ast
592592

src/basilisp/lang/reader.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def format_syntax_error( # pylint: disable=unused-argument
177177
else:
178178
lines.append(f" message: {e.message}: {context_exc}{os.linesep}")
179179

180-
if e.line is not None and e.col:
180+
if e.line is not None and e.col is not None:
181181
line_num = f"{e.line}:{e.col}"
182182
elif e.line is not None:
183183
line_num = str(e.line)
@@ -225,37 +225,38 @@ def __init__(self, stream: io.TextIOBase, pushback_depth: int = 5) -> None:
225225
self._stream = stream
226226
self._pushback_depth = pushback_depth
227227
self._idx = -2
228-
init_buffer = [self._stream.read(1), self._stream.read(1)]
229-
self._buffer = collections.deque(init_buffer, pushback_depth)
230228
self._line = collections.deque([1], pushback_depth)
231-
self._col = collections.deque([1], pushback_depth)
229+
self._col = collections.deque([0], pushback_depth)
230+
self._buffer = collections.deque([self._stream.read(1)], pushback_depth)
232231

233-
for c in init_buffer[1:]:
234-
self._update_loc(c)
232+
# Load up an extra character
233+
self._buffer.append(self._stream.read(1))
234+
self._update_loc()
235235

236236
@property
237237
def name(self) -> Optional[str]:
238238
return getattr(self._stream, "name", None)
239239

240240
@property
241241
def col(self) -> int:
242+
"""Return the column of the character returned by `peek`."""
242243
return self._col[self._idx]
243244

244245
@property
245246
def line(self) -> int:
247+
"""Return the line of the character returned by `peek`."""
246248
return self._line[self._idx]
247249

248250
@property
249251
def loc(self) -> Tuple[int, int]:
252+
"""Return the location of the character returned by `peek` as a tuple of
253+
(line, col)."""
250254
return self.line, self.col
251255

252-
def _update_loc(self, c):
253-
"""Update the internal line and column buffers after a new character
254-
is added.
255-
256-
The column number is set to 0, so the first character on the next line
257-
is column number 1."""
258-
if newline_chars.match(c):
256+
def _update_loc(self):
257+
"""Update the internal line and column buffers after a new character is
258+
added."""
259+
if newline_chars.match(self._buffer[-2]):
259260
self._col.append(0)
260261
self._line.append(self._line[-1] + 1)
261262
else:
@@ -289,8 +290,8 @@ def next_char(self) -> str:
289290
self._idx += 1
290291
else:
291292
c = self._stream.read(1)
292-
self._update_loc(c)
293293
self._buffer.append(c)
294+
self._update_loc()
294295
return self.peek()
295296

296297

src/basilisp/lang/source.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import itertools
12
import linecache
23
import os
3-
from typing import List, Optional
4+
from typing import Iterable, List, Optional
45

56
try:
67
import pygments.formatters
@@ -40,7 +41,7 @@ def _format_source(s: str) -> str: # pragma: no cover
4041
)
4142

4243

43-
def format_source_context(
44+
def format_source_context( # pylint: disable=too-many-locals
4445
filename: str,
4546
line: int,
4647
end_line: Optional[int] = None,
@@ -58,15 +59,25 @@ def format_source_context(
5859
if not show_cause_marker:
5960
cause_range = None
6061
elif end_line is not None and end_line != line:
61-
cause_range = range(line, end_line)
62+
cause_range = range(line, end_line + 1)
6263
else:
6364
cause_range = range(line, line + 1)
6465

6566
if source_lines := linecache.getlines(filename):
66-
start = max(0, line - num_context_lines)
67-
end = min((end_line or line) + num_context_lines, len(source_lines))
67+
selected_lines: Iterable[str]
68+
if end_line is None and line > len(source_lines):
69+
end = len(source_lines) + 1
70+
start = max(end - num_context_lines, 0)
71+
selected_lines = itertools.chain(
72+
source_lines[start:end], itertools.repeat("\n")
73+
)
74+
else:
75+
start = max(0, line - num_context_lines)
76+
end = min((end_line or line) + num_context_lines, len(source_lines))
77+
selected_lines = source_lines[start:end]
78+
6879
num_justify = max(len(str(start)), len(str(end))) + 1
69-
for n, source_line in zip(range(start, end), source_lines[start:end]):
80+
for n, source_line in zip(range(start, end), selected_lines):
7081
if cause_range is None:
7182
line_marker = " "
7283
elif n + 1 in cause_range:

tests/basilisp/cli_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_syntax_error(self, run_cli):
196196
"",
197197
" exception: <class 'basilisp.lang.reader.UnexpectedEOFError'>",
198198
" message: Unexpected EOF in list",
199-
" line: 1:7",
199+
" line: 1:6",
200200
"",
201201
)
202202
)

tests/basilisp/compiler_test.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,13 @@ def source_file(self, tmp_path: Path) -> Path:
201201
def compiler_file_path(self, source_file: Path) -> str:
202202
return str(source_file)
203203

204+
@pytest.mark.parametrize("include_newline", [True, False])
204205
def test_shows_source_context(
205-
self, monkeypatch, source_file: Path, lcompile: CompileFn
206+
self,
207+
monkeypatch,
208+
source_file: Path,
209+
lcompile: CompileFn,
210+
include_newline: bool,
206211
):
207212
source_file.write_text(
208213
textwrap.dedent(
@@ -213,29 +218,31 @@ def test_shows_source_context(
213218
b)
214219
"""
215220
).lstrip()
221+
+ ("\n" if include_newline else "")
216222
)
217223
monkeypatch.setenv("BASILISP_NO_COLOR", "true")
218224
monkeypatch.syspath_prepend(source_file.parent)
219225

220226
with pytest.raises(compiler.CompilerException) as e:
221227
lcompile("(require 'compiler-test)")
222228

229+
formatted = format_exception(e.value)
223230
assert re.match(
224231
(
225232
rf"{os.linesep}"
226233
rf" exception: <class 'basilisp.lang.compiler.exception.CompilerException'> from <class 'basilisp.lang.compiler.exception.CompilerException'>{os.linesep}"
227234
rf" phase: :macroexpansion{os.linesep}"
228235
rf" message: error occurred during macroexpansion: unable to resolve symbol 'b' in this context{os.linesep}"
229-
rf" form: \(let \[a :b] b\){os.linesep}"
230-
rf" location: (?:\w:)?[^:]*:3-5{os.linesep}"
236+
rf" form: \(let \[a :b\] b\){os.linesep}"
237+
rf" location: (?:\w:)?[^:]*:3-4{os.linesep}"
231238
rf" context:{os.linesep}"
232239
rf"{os.linesep}"
233240
rf" 1 \| \(ns compiler-test\){os.linesep}"
234241
rf" 2 \| {os.linesep}"
235242
rf" 3 > \| \(let \[a :b]{os.linesep}"
236243
rf" 4 > \| b\){os.linesep}"
237244
),
238-
"".join(format_exception(e.value)),
245+
"".join(formatted),
239246
)
240247

241248

@@ -470,7 +477,7 @@ def test_compiler_metadata(
470477

471478
assert 1 == meta.val_at(kw.keyword("line"))
472479
assert compiler_file_path == meta.val_at(kw.keyword("file"))
473-
assert 1 == meta.val_at(kw.keyword("col"))
480+
assert 0 == meta.val_at(kw.keyword("col"))
474481
assert sym.symbol("unique-oeuene") == meta.val_at(kw.keyword("name"))
475482
assert ns == meta.val_at(kw.keyword("ns"))
476483
assert "Super cool docstring" == meta.val_at(kw.keyword("doc"))

0 commit comments

Comments
 (0)