Skip to content

Commit e10c19f

Browse files
committed
Pass through source position information in spans
Prior to this change, we were tracking source position information in the stream, but were not storing it in the resulting spans. This change adds start_position and end_position SourcePosition attributes to Span nodes. To make this work we end up complicating the constructor for Span nodes, so that it can either take a location or an index/position combo (for JSON deserialization).
1 parent d641131 commit e10c19f

File tree

5 files changed

+70
-10
lines changed

5 files changed

+70
-10
lines changed

fluent.syntax/fluent/syntax/ast.py

+34-5
Original file line numberDiff line numberDiff line change
@@ -388,18 +388,47 @@ def __init__(self, row_index: int, column_index: int, **kwargs: Any):
388388
self.column_index = column_index
389389

390390
class Span(BaseNode):
391-
def __init__(self, start: Location, end: Location, **kwargs: Any):
391+
def __init__(
392+
self,
393+
start: Location | int,
394+
end: Location | int,
395+
start_position: SourcePosition | None = None,
396+
end_position: SourcePosition | None = None,
397+
**kwargs: Any,
398+
):
392399
super().__init__(**kwargs)
393-
self.start = start
394-
self.end = end
400+
401+
# We support two forms of arguments to the constructor. This is to allow the parser to use
402+
# Location tuples for convenience, but to allow position objects to be passed during json
403+
# deserialization
404+
start_index, start_position = self._coerce_location_and_position(start, start_position)
405+
end_index, end_position = self._coerce_location_and_position(end, end_position)
406+
407+
self.start = start_index
408+
self.end = end_index
409+
self.start_position = start_position
410+
self.end_position = end_position
411+
412+
def _coerce_location_and_position(
413+
self,
414+
location_or_index: Location | int,
415+
position: SourcePosition | None,
416+
) -> tuple[int, SourcePosition]:
417+
if isinstance(location_or_index, int):
418+
assert position is not None, "position must be passed if location is not passed"
419+
return location_or_index, position
420+
else:
421+
assert position is None, "position must not be passed if location is passed"
422+
index, row, column = location_or_index
423+
return index, SourcePosition(row, column)
395424

396425
@property
397426
def start_location(self) -> Location:
398-
return self.start
427+
return self.start, self.start_position.row_index, self.start_position.column_index
399428

400429
@property
401430
def end_location(self) -> Location:
402-
return self.end
431+
return self.end, self.end_position.row_index, self.end_position.column_index
403432

404433

405434
class Annotation(SyntaxNode):

fluent.syntax/fluent/syntax/parser.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def parse(self, source: str) -> ast.Resource:
8585
res = ast.Resource(entries)
8686

8787
if self.with_spans:
88-
res.add_span(0, ps.current_location)
88+
res.add_span((0, 0, 0), ps.current_location)
8989

9090
return res
9191

fluent.syntax/fluent/syntax/stream.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
# Represents a location in the parser stream (for convenience)
99
# - Index
10-
Location: TypeAlias = int
10+
# - Row index (in source)
11+
# - Column index (in source)
12+
Location: TypeAlias = tuple[int, int, int]
1113

1214

1315
class ParserStream:
@@ -38,7 +40,7 @@ def char_at(self, offset: int) -> Union[str, None]:
3840

3941
@property
4042
def current_location(self) -> Location:
41-
return self.index
43+
return self.index, self.row_index, self.column_index
4244

4345
@property
4446
def current_char(self) -> Union[str, None]:

fluent.syntax/tests/syntax/test_entry.py

+30-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,21 @@ def test_return_junk(self):
6565
"arguments": ["="],
6666
"code": "E0003",
6767
"message": 'Expected token: "="',
68-
"span": {"end": 23, "start": 23, "type": "Span"},
68+
"span": {
69+
"end": 23,
70+
"start": 23,
71+
"end_position": {
72+
"column_index": 4,
73+
"row_index": 1,
74+
"type": "SourcePosition",
75+
},
76+
"start_position": {
77+
"column_index": 4,
78+
"row_index": 1,
79+
"type": "SourcePosition",
80+
},
81+
"type": "Span"
82+
},
6983
"type": "Annotation",
7084
}
7185
],
@@ -111,7 +125,21 @@ def test_do_not_ignore_invalid_comments(self):
111125
"arguments": [" "],
112126
"code": "E0003",
113127
"message": 'Expected token: " "',
114-
"span": {"end": 21, "start": 21, "type": "Span"},
128+
"span": {
129+
"end": 21,
130+
"start": 21,
131+
"end_position": {
132+
"column_index": 2,
133+
"row_index": 1,
134+
"type": "SourcePosition",
135+
},
136+
"start_position": {
137+
"column_index": 2,
138+
"row_index": 1,
139+
"type": "SourcePosition",
140+
},
141+
"type": "Span",
142+
},
115143
"type": "Annotation",
116144
}
117145
],

fluent.syntax/tests/syntax/test_visitor.py

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def test_resource(self):
4444
"Identifier": 4,
4545
"Attribute": 1,
4646
"Span": 10,
47+
"SourcePosition": 20,
4748
},
4849
)
4950

0 commit comments

Comments
 (0)