Skip to content

Commit 8e9075c

Browse files
sei40krclaude
authored andcommitted
feat: add start_line, end_line, start_column, end_column, and chop methods for Location
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c5882ce commit 8e9075c

4 files changed

Lines changed: 115 additions & 1 deletion

File tree

rust/ruby-prism-sys/build/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ fn generate_bindings(ruby_include_path: &Path) -> bindgen::Bindings {
118118
.allowlist_type("pm_diagnostic_t")
119119
.allowlist_type("pm_list_t")
120120
.allowlist_type("pm_magic_comment_t")
121+
.allowlist_type("pm_line_offset_list_t")
121122
.allowlist_type("pm_node_t")
122123
.allowlist_type("pm_node_type")
123124
.allowlist_type("pm_parser_t")

rust/ruby-prism-sys/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,25 @@ mod bindings {
3535
}
3636

3737
pub use self::bindings::*;
38+
39+
/// Line and column information for a given byte offset relative to the
40+
/// beginning of the source.
41+
#[repr(C)]
42+
#[derive(Debug, Clone, Copy)]
43+
pub struct pm_line_column_t {
44+
/// The 1-indexed line number relative to the start line configured on the
45+
/// parser.
46+
pub line: i32,
47+
/// The 0-indexed column number in bytes.
48+
pub column: u32,
49+
}
50+
51+
extern "C" {
52+
/// Return the line and column number for the given byte offset relative to
53+
/// the beginning of the source.
54+
pub fn pm_line_offset_list_line_column(
55+
list: *const pm_line_offset_list_t,
56+
cursor: u32,
57+
start_line: i32,
58+
) -> pm_line_column_t;
59+
}

rust/ruby-prism/src/lib.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,48 @@ mod tests {
181181
assert_eq!(slice, "222");
182182
}
183183

184+
#[test]
185+
#[allow(clippy::similar_names)]
186+
fn location_line_column_test() {
187+
let source = "foo\nbar\nbaz";
188+
let result = parse(source.as_ref());
189+
190+
let node = result.node();
191+
let program = node.as_program_node().unwrap();
192+
let statements = program.statements().body();
193+
let mut iter = statements.iter();
194+
let _foo = iter.next().unwrap();
195+
let bar = iter.next().unwrap();
196+
let baz = iter.next().unwrap();
197+
198+
let bar_loc = bar.location();
199+
assert_eq!(bar_loc.start_line(), 2);
200+
assert_eq!(bar_loc.end_line(), 2);
201+
assert_eq!(bar_loc.start_column(), 0);
202+
assert_eq!(bar_loc.end_column(), 3);
203+
204+
let baz_loc = baz.location();
205+
assert_eq!(baz_loc.start_line(), 3);
206+
assert_eq!(baz_loc.end_line(), 3);
207+
assert_eq!(baz_loc.start_column(), 0);
208+
assert_eq!(baz_loc.end_column(), 3);
209+
}
210+
211+
#[test]
212+
fn test_chop() {
213+
let result = parse(b"foo");
214+
let mut location = result.node().as_program_node().unwrap().location();
215+
216+
assert_eq!(location.chop().as_slice(), b"fo");
217+
assert_eq!(location.chop().chop().chop().as_slice(), b"");
218+
219+
// Check that we don't go negative.
220+
for _ in 0..10 {
221+
location = location.chop();
222+
}
223+
assert_eq!(location.as_slice(), b"");
224+
}
225+
184226
#[test]
185227
fn visitor_test() {
186228
use super::{visit_interpolated_regular_expression_node, visit_regular_expression_node, InterpolatedRegularExpressionNode, RegularExpressionNode, Visit};

rust/ruby-prism/src/parse_result/mod.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod diagnostics;
88

99
use std::ptr::NonNull;
1010

11-
use ruby_prism_sys::{pm_comment_t, pm_diagnostic_t, pm_location_t, pm_magic_comment_t, pm_node_destroy, pm_node_t, pm_parser_free, pm_parser_t};
11+
use ruby_prism_sys::{pm_comment_t, pm_diagnostic_t, pm_line_column_t, pm_line_offset_list_line_column, pm_location_t, pm_magic_comment_t, pm_node_destroy, pm_node_t, pm_parser_free, pm_parser_t};
1212

1313
pub use self::comments::{Comment, CommentType, Comments, MagicComment, MagicComments};
1414
pub use self::diagnostics::{Diagnostic, Diagnostics};
@@ -66,6 +66,55 @@ impl<'pr> Location<'pr> {
6666
})
6767
}
6868
}
69+
70+
/// Returns the line number where this location starts.
71+
#[must_use]
72+
pub fn start_line(&self) -> i32 {
73+
self.line_column_at(self.start).line
74+
}
75+
76+
/// Returns the line number where this location ends.
77+
#[must_use]
78+
pub fn end_line(&self) -> i32 {
79+
self.line_column_at(self.end()).line
80+
}
81+
82+
/// Returns the column number in bytes where this location starts from the
83+
/// start of the line.
84+
#[must_use]
85+
pub fn start_column(&self) -> u32 {
86+
self.line_column_at(self.start).column
87+
}
88+
89+
/// Returns the column number in bytes where this location ends from the
90+
/// start of the line.
91+
#[must_use]
92+
pub fn end_column(&self) -> u32 {
93+
self.line_column_at(self.end()).column
94+
}
95+
96+
/// Returns a new location that is the result of chopping off the last byte.
97+
#[must_use]
98+
pub const fn chop(&self) -> Self {
99+
Location {
100+
parser: self.parser,
101+
start: self.start,
102+
length: if self.length == 0 { 0 } else { self.length - 1 },
103+
marker: std::marker::PhantomData,
104+
}
105+
}
106+
107+
fn line_column_at(&self, offset: u32) -> pm_line_column_t {
108+
// SAFETY: `self.parser` is a valid pointer to a `pm_parser_t` that
109+
// outlives `self`, and `pm_line_offset_list_line_column` only reads
110+
// from the line offset list.
111+
unsafe {
112+
let parser = self.parser.as_ptr();
113+
let line_offsets = &(*parser).line_offsets;
114+
let start_line = (*parser).start_line;
115+
pm_line_offset_list_line_column(line_offsets, offset, start_line)
116+
}
117+
}
69118
}
70119

71120
impl std::fmt::Debug for Location<'_> {

0 commit comments

Comments
 (0)