ruby_prism/parse_result/
mod.rs

1//! Parse result types for the prism parser.
2
3mod comments;
4mod diagnostics;
5
6use std::ptr::NonNull;
7
8use ruby_prism_sys::{
9    pm_arena_free, pm_arena_t, pm_comment_t, pm_diagnostic_t, pm_line_offset_list_line_column, pm_location_t, pm_magic_comment_t, pm_node_t, pm_parser_comments_each, pm_parser_comments_size, pm_parser_data_loc, pm_parser_errors_each, pm_parser_errors_size, pm_parser_free,
10    pm_parser_frozen_string_literal, pm_parser_line_offsets, pm_parser_magic_comments_each, pm_parser_magic_comments_size, pm_parser_start, pm_parser_start_line, pm_parser_t, pm_parser_warnings_each, pm_parser_warnings_size,
11};
12
13pub use self::comments::{Comment, CommentType, Comments, MagicComment, MagicComments};
14pub use self::diagnostics::{Diagnostic, Diagnostics};
15
16use crate::Node;
17
18/// A range in the source file, represented as a start offset and length.
19pub struct Location<'pr> {
20    pub(crate) parser: *const pm_parser_t,
21    pub(crate) start: u32,
22    pub(crate) length: u32,
23    marker: std::marker::PhantomData<&'pr [u8]>,
24}
25
26impl<'pr> Location<'pr> {
27    /// Returns a byte slice for the range.
28    #[must_use]
29    pub fn as_slice(&self) -> &'pr [u8] {
30        unsafe {
31            let parser_start = pm_parser_start(self.parser);
32            std::slice::from_raw_parts(parser_start.add(self.start as usize), self.length as usize)
33        }
34    }
35
36    /// Return a Location from the given `pm_location_t`.
37    #[must_use]
38    pub(crate) const fn new(parser: *const pm_parser_t, location: &'pr pm_location_t) -> Self {
39        Location {
40            parser,
41            start: location.start,
42            length: location.length,
43            marker: std::marker::PhantomData,
44        }
45    }
46
47    /// Returns the start offset from the beginning of the parsed source.
48    #[must_use]
49    pub const fn start(&self) -> u32 {
50        self.start
51    }
52
53    /// Returns the end offset from the beginning of the parsed source.
54    #[must_use]
55    pub const fn end(&self) -> u32 {
56        self.start + self.length
57    }
58
59    /// Return a Location starting at self and ending at the end of other.
60    /// Returns None if both locations did not originate from the same parser,
61    /// or if self starts after other.
62    #[must_use]
63    pub fn join(&self, other: &Self) -> Option<Self> {
64        if self.parser != other.parser || self.start > other.start {
65            None
66        } else {
67            Some(Location {
68                parser: self.parser,
69                start: self.start,
70                length: other.end() - self.start,
71                marker: std::marker::PhantomData,
72            })
73        }
74    }
75
76    /// Returns a new location that is the result of chopping off the last byte.
77    #[must_use]
78    pub const fn chop(&self) -> Self {
79        Location {
80            parser: self.parser,
81            start: self.start,
82            length: if self.length == 0 { 0 } else { self.length - 1 },
83            marker: std::marker::PhantomData,
84        }
85    }
86}
87
88impl Location<'_> {
89    /// Returns the line number where this location starts.
90    #[must_use]
91    pub fn start_line(&self) -> i32 {
92        self.line_column(self.start).0
93    }
94
95    /// Returns the column number in bytes where this location starts from the
96    /// start of the line.
97    #[must_use]
98    pub fn start_column(&self) -> u32 {
99        self.line_column(self.start).1
100    }
101
102    /// Returns the line number where this location ends.
103    #[must_use]
104    pub fn end_line(&self) -> i32 {
105        self.line_column(self.end()).0
106    }
107
108    /// Returns the column number in bytes where this location ends from the
109    /// start of the line.
110    #[must_use]
111    pub fn end_column(&self) -> u32 {
112        self.line_column(self.end()).1
113    }
114
115    /// Returns the line and column number for the given byte offset.
116    fn line_column(&self, cursor: u32) -> (i32, u32) {
117        unsafe {
118            let line_offsets = pm_parser_line_offsets(self.parser);
119            let start_line = pm_parser_start_line(self.parser);
120            let result = pm_line_offset_list_line_column(line_offsets, cursor, start_line);
121            (result.line, result.column)
122        }
123    }
124}
125
126impl std::fmt::Debug for Location<'_> {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        let slice: &[u8] = self.as_slice();
129
130        let mut visible = String::new();
131        visible.push('"');
132
133        for &byte in slice {
134            let part: Vec<u8> = std::ascii::escape_default(byte).collect();
135            visible.push_str(std::str::from_utf8(&part).unwrap());
136        }
137
138        visible.push('"');
139        write!(f, "{visible}")
140    }
141}
142
143// C callback that collects comment pointers into a Vec
144unsafe extern "C" fn collect_comment(comment: *const pm_comment_t, data: *mut std::ffi::c_void) {
145    let vec = &mut *(data.cast::<Vec<*const pm_comment_t>>());
146    vec.push(comment);
147}
148
149// C callback that collects magic comment pointers into a Vec
150unsafe extern "C" fn collect_magic_comment(comment: *const pm_magic_comment_t, data: *mut std::ffi::c_void) {
151    let vec = &mut *(data.cast::<Vec<*const pm_magic_comment_t>>());
152    vec.push(comment);
153}
154
155// C callback that collects diagnostic pointers into a Vec
156unsafe extern "C" fn collect_diagnostic(diagnostic: *const pm_diagnostic_t, data: *mut std::ffi::c_void) {
157    let vec = &mut *(data.cast::<Vec<*const pm_diagnostic_t>>());
158    vec.push(diagnostic);
159}
160
161/// The result of parsing a source string.
162#[derive(Debug)]
163pub struct ParseResult<'pr> {
164    source: &'pr [u8],
165    arena: *mut pm_arena_t,
166    parser: *mut pm_parser_t,
167    node: NonNull<pm_node_t>,
168}
169
170impl<'pr> ParseResult<'pr> {
171    pub(crate) const unsafe fn new(source: &'pr [u8], arena: *mut pm_arena_t, parser: *mut pm_parser_t, node: NonNull<pm_node_t>) -> Self {
172        ParseResult { source, arena, parser, node }
173    }
174
175    /// Returns the source string that was parsed.
176    #[must_use]
177    pub const fn source(&self) -> &'pr [u8] {
178        self.source
179    }
180
181    /// Returns whether we found a `frozen_string_literal` magic comment with a true value.
182    #[must_use]
183    pub fn frozen_string_literals(&self) -> bool {
184        unsafe { pm_parser_frozen_string_literal(self.parser) == 1 }
185    }
186
187    /// Returns a slice of the source string that was parsed using the given
188    /// slice range.
189    #[must_use]
190    pub fn as_slice(&self, location: &Location<'pr>) -> &'pr [u8] {
191        let start = location.start as usize;
192        let end = start + location.length as usize;
193        &self.source[start..end]
194    }
195
196    /// Returns a slice containing the offsets of the start of each line in the source string
197    /// that was parsed.
198    #[must_use]
199    pub fn line_offsets(&self) -> &'pr [u32] {
200        unsafe {
201            let list = &*pm_parser_line_offsets(self.parser);
202            std::slice::from_raw_parts(list.offsets, list.size)
203        }
204    }
205
206    /// Returns an iterator that can be used to iterate over the errors in the
207    /// parse result.
208    #[must_use]
209    pub fn errors(&self) -> Diagnostics<'_> {
210        let size = unsafe { pm_parser_errors_size(self.parser) };
211        let mut ptrs: Vec<*const pm_diagnostic_t> = Vec::with_capacity(size);
212        unsafe {
213            pm_parser_errors_each(self.parser, Some(collect_diagnostic), (&raw mut ptrs).cast());
214        }
215        Diagnostics::new(ptrs, self.parser)
216    }
217
218    /// Returns an iterator that can be used to iterate over the warnings in the
219    /// parse result.
220    #[must_use]
221    pub fn warnings(&self) -> Diagnostics<'_> {
222        let size = unsafe { pm_parser_warnings_size(self.parser) };
223        let mut ptrs: Vec<*const pm_diagnostic_t> = Vec::with_capacity(size);
224        unsafe {
225            pm_parser_warnings_each(self.parser, Some(collect_diagnostic), (&raw mut ptrs).cast());
226        }
227        Diagnostics::new(ptrs, self.parser)
228    }
229
230    /// Returns an iterator that can be used to iterate over the comments in the
231    /// parse result.
232    #[must_use]
233    pub fn comments(&self) -> Comments<'_> {
234        let size = unsafe { pm_parser_comments_size(self.parser) };
235        let mut ptrs: Vec<*const pm_comment_t> = Vec::with_capacity(size);
236        unsafe {
237            pm_parser_comments_each(self.parser, Some(collect_comment), (&raw mut ptrs).cast());
238        }
239        Comments::new(ptrs, self.parser)
240    }
241
242    /// Returns an iterator that can be used to iterate over the magic comments in the
243    /// parse result.
244    #[must_use]
245    pub fn magic_comments(&self) -> MagicComments<'_> {
246        let size = unsafe { pm_parser_magic_comments_size(self.parser) };
247        let mut ptrs: Vec<*const pm_magic_comment_t> = Vec::with_capacity(size);
248        unsafe {
249            pm_parser_magic_comments_each(self.parser, Some(collect_magic_comment), (&raw mut ptrs).cast());
250        }
251        MagicComments::new(ptrs, self.parser)
252    }
253
254    /// Returns an optional location of the __END__ marker and the rest of the content of the file.
255    #[must_use]
256    pub fn data_loc(&self) -> Option<Location<'_>> {
257        let location = unsafe { &*pm_parser_data_loc(self.parser) };
258        if location.length == 0 {
259            None
260        } else {
261            Some(Location::new(self.parser, location))
262        }
263    }
264
265    /// Returns the root node of the parse result.
266    #[must_use]
267    pub fn node(&self) -> Node<'_> {
268        Node::new(self.parser, self.node.as_ptr())
269    }
270
271    /// Returns true if there were no errors during parsing and false if there
272    /// were.
273    #[must_use]
274    pub fn is_success(&self) -> bool {
275        self.errors().next().is_none()
276    }
277
278    /// Returns true if there were errors during parsing and false if there were
279    /// not.
280    #[must_use]
281    pub fn is_failure(&self) -> bool {
282        !self.is_success()
283    }
284}
285
286impl Drop for ParseResult<'_> {
287    fn drop(&mut self) {
288        unsafe {
289            pm_parser_free(self.parser);
290            pm_arena_free(self.arena);
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use crate::parse;
298
299    #[test]
300    fn test_is_success() {
301        let result = parse(b"1 + 1");
302        assert!(result.is_success());
303        assert!(!result.is_failure());
304    }
305
306    #[test]
307    fn test_is_failure() {
308        let result = parse(b"<>");
309        assert!(result.is_failure());
310        assert!(!result.is_success());
311    }
312}