From 348b54d76aad7f90206cd76f8adec3f48ccb3196 Mon Sep 17 00:00:00 2001 From: Seong Yong-ju Date: Thu, 29 Jan 2026 17:58:34 +0900 Subject: [PATCH] feat: add `start_line`, `end_line`, `start_column`, `end_column`, and `chop` methods for `Location` Co-Authored-By: Claude Opus 4.5 --- rust/ruby-prism-sys/build/main.rs | 3 ++ rust/ruby-prism/src/lib.rs | 41 +++++++++++++++++++ rust/ruby-prism/src/parse_result/mod.rs | 54 ++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/rust/ruby-prism-sys/build/main.rs b/rust/ruby-prism-sys/build/main.rs index 798d06d8ff..971b5e951e 100644 --- a/rust/ruby-prism-sys/build/main.rs +++ b/rust/ruby-prism-sys/build/main.rs @@ -116,6 +116,7 @@ fn generate_bindings(ruby_include_path: &Path) -> bindgen::Bindings { // Structs .allowlist_type("pm_comment_t") .allowlist_type("pm_diagnostic_t") + .allowlist_type("pm_line_column_t") .allowlist_type("pm_list_t") .allowlist_type("pm_magic_comment_t") .allowlist_type("pm_node_t") @@ -140,6 +141,8 @@ fn generate_bindings(ruby_include_path: &Path) -> bindgen::Bindings { // Functions .allowlist_function("pm_list_empty_p") .allowlist_function("pm_list_free") + .allowlist_function("pm_newline_list_line") + .allowlist_function("pm_newline_list_line_column") .allowlist_function("pm_node_destroy") .allowlist_function("pm_pack_parse") .allowlist_function("pm_parse") diff --git a/rust/ruby-prism/src/lib.rs b/rust/ruby-prism/src/lib.rs index 6824768193..dae4cde333 100644 --- a/rust/ruby-prism/src/lib.rs +++ b/rust/ruby-prism/src/lib.rs @@ -160,6 +160,47 @@ mod tests { assert_eq!(slice, "222"); } + #[test] + fn location_line_column_test() { + let source = "foo\nbar\nbaz"; + let result = parse(source.as_ref()); + + let node = result.node(); + let program = node.as_program_node().unwrap(); + let statements = program.statements().body(); + let mut iter = statements.iter(); + let _foo = iter.next().unwrap(); + let bar = iter.next().unwrap(); + let baz = iter.next().unwrap(); + + let bar_loc = bar.location(); + assert_eq!(bar_loc.start_line(), 2); + assert_eq!(bar_loc.end_line(), 2); + assert_eq!(bar_loc.start_column(), 0); + assert_eq!(bar_loc.end_column(), 3); + + let baz_loc = baz.location(); + assert_eq!(baz_loc.start_line(), 3); + assert_eq!(baz_loc.end_line(), 3); + assert_eq!(baz_loc.start_column(), 0); + assert_eq!(baz_loc.end_column(), 3); + } + + #[test] + fn test_chop() { + let result = parse(b"foo"); + let mut location = result.node().as_program_node().unwrap().location(); + + assert_eq!(location.chop().as_slice(), b"fo"); + assert_eq!(location.chop().chop().chop().as_slice(), b""); + + // Check that we don't go negative. + for _ in 0..10 { + location = location.chop(); + } + assert_eq!(location.as_slice(), b""); + } + #[test] fn visitor_test() { use super::{visit_interpolated_regular_expression_node, visit_regular_expression_node, InterpolatedRegularExpressionNode, RegularExpressionNode, Visit}; diff --git a/rust/ruby-prism/src/parse_result/mod.rs b/rust/ruby-prism/src/parse_result/mod.rs index 33eb1ac9a0..447b47e460 100644 --- a/rust/ruby-prism/src/parse_result/mod.rs +++ b/rust/ruby-prism/src/parse_result/mod.rs @@ -8,7 +8,7 @@ mod diagnostics; use std::ptr::NonNull; -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}; +use ruby_prism_sys::{pm_comment_t, pm_diagnostic_t, pm_location_t, pm_magic_comment_t, pm_newline_list_line_column, pm_node_destroy, pm_node_t, pm_parser_free, pm_parser_t}; pub use self::comments::{Comment, CommentType, Comments, MagicComment, MagicComments}; pub use self::diagnostics::{Diagnostic, Diagnostics}; @@ -66,6 +66,58 @@ impl<'pr> Location<'pr> { }) } } + + /// Returns the line number where this location starts. + #[must_use] + pub fn start_line(&self) -> i32 { + self.line_column_at(self.start).line + } + + /// Returns the line number where this location ends. + #[must_use] + pub fn end_line(&self) -> i32 { + self.line_column_at(self.end()).line + } + + /// Returns the column number in bytes where this location starts from the + /// start of the line. + #[must_use] + pub fn start_column(&self) -> u32 { + self.line_column_at(self.start).column + } + + /// Returns the column number in bytes where this location ends from the + /// start of the line. + #[must_use] + pub fn end_column(&self) -> u32 { + self.line_column_at(self.end()).column + } + + /// Returns a new location that is the result of chopping off the last byte. + #[must_use] + pub const fn chop(&self) -> Self { + Location { + parser: self.parser, + start: self.start, + length: if self.length == 0 { 0 } else { self.length - 1 }, + marker: std::marker::PhantomData, + } + } + + fn line_column_at(&self, offset: u32) -> LineColumn { + unsafe { + let parser = self.parser.as_ptr(); + let newline_list = &(*parser).newline_list; + let start_line = (*parser).start_line; + let result = pm_newline_list_line_column(newline_list, offset, start_line); + LineColumn { line: result.line, column: result.column } + } + } +} + +struct LineColumn { + line: i32, + column: u32, } impl std::fmt::Debug for Location<'_> {