Skip to main content

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_next, pm_diagnostic_next, pm_line_offset_list_line_column, pm_location_t, pm_magic_comment_next, pm_node_t, pm_parser_comments_first, pm_parser_comments_size, pm_parser_data_loc, pm_parser_errors_first, pm_parser_errors_size, pm_parser_free,
10    pm_parser_frozen_string_literal, pm_parser_line_offsets, pm_parser_magic_comments_first, pm_parser_magic_comments_size, pm_parser_start, pm_parser_start_line, pm_parser_t, pm_parser_warnings_first, 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// Collects the items of one of the parser's lists into a Vec, starting from the
144// first item and following each item's successor until it is null.
145unsafe fn collect<T>(size: usize, first: *const T, next: unsafe extern "C" fn(*const T) -> *const T) -> Vec<*const T> {
146    let mut ptrs = Vec::with_capacity(size);
147    let mut current = first;
148    while !current.is_null() {
149        ptrs.push(current);
150        current = next(current);
151    }
152    ptrs
153}
154
155/// The result of parsing a source string.
156#[derive(Debug)]
157pub struct ParseResult<'pr> {
158    source: &'pr [u8],
159    arena: *mut pm_arena_t,
160    parser: *mut pm_parser_t,
161    node: NonNull<pm_node_t>,
162}
163
164impl<'pr> ParseResult<'pr> {
165    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 {
166        ParseResult { source, arena, parser, node }
167    }
168
169    /// Returns the source string that was parsed.
170    #[must_use]
171    pub const fn source(&self) -> &'pr [u8] {
172        self.source
173    }
174
175    /// Returns whether we found a `frozen_string_literal` magic comment with a true value.
176    #[must_use]
177    pub fn frozen_string_literals(&self) -> bool {
178        unsafe { pm_parser_frozen_string_literal(self.parser) == 1 }
179    }
180
181    /// Returns a slice of the source string that was parsed using the given
182    /// slice range.
183    #[must_use]
184    pub fn as_slice(&self, location: &Location<'pr>) -> &'pr [u8] {
185        let start = location.start as usize;
186        let end = start + location.length as usize;
187        &self.source[start..end]
188    }
189
190    /// Returns a slice containing the offsets of the start of each line in the source string
191    /// that was parsed.
192    #[must_use]
193    pub fn line_offsets(&self) -> &'pr [u32] {
194        unsafe {
195            let list = &*pm_parser_line_offsets(self.parser);
196            std::slice::from_raw_parts(list.offsets, list.size)
197        }
198    }
199
200    /// Returns an iterator that can be used to iterate over the errors in the
201    /// parse result.
202    #[must_use]
203    pub fn errors(&self) -> Diagnostics<'_> {
204        let ptrs = unsafe { collect(pm_parser_errors_size(self.parser), pm_parser_errors_first(self.parser), pm_diagnostic_next) };
205        Diagnostics::new(ptrs, self.parser)
206    }
207
208    /// Returns an iterator that can be used to iterate over the warnings in the
209    /// parse result.
210    #[must_use]
211    pub fn warnings(&self) -> Diagnostics<'_> {
212        let ptrs = unsafe { collect(pm_parser_warnings_size(self.parser), pm_parser_warnings_first(self.parser), pm_diagnostic_next) };
213        Diagnostics::new(ptrs, self.parser)
214    }
215
216    /// Returns an iterator that can be used to iterate over the comments in the
217    /// parse result.
218    #[must_use]
219    pub fn comments(&self) -> Comments<'_> {
220        let ptrs = unsafe { collect(pm_parser_comments_size(self.parser), pm_parser_comments_first(self.parser), pm_comment_next) };
221        Comments::new(ptrs, self.parser)
222    }
223
224    /// Returns an iterator that can be used to iterate over the magic comments in the
225    /// parse result.
226    #[must_use]
227    pub fn magic_comments(&self) -> MagicComments<'_> {
228        let ptrs = unsafe { collect(pm_parser_magic_comments_size(self.parser), pm_parser_magic_comments_first(self.parser), pm_magic_comment_next) };
229        MagicComments::new(ptrs, self.parser)
230    }
231
232    /// Returns an optional location of the __END__ marker and the rest of the content of the file.
233    #[must_use]
234    pub fn data_loc(&self) -> Option<Location<'_>> {
235        let location = unsafe { &*pm_parser_data_loc(self.parser) };
236        if location.length == 0 {
237            None
238        } else {
239            Some(Location::new(self.parser, location))
240        }
241    }
242
243    /// Returns the root node of the parse result.
244    #[must_use]
245    pub fn node(&self) -> Node<'_> {
246        Node::new(self.parser, self.node.as_ptr())
247    }
248
249    /// Returns true if there were no errors during parsing and false if there
250    /// were.
251    #[must_use]
252    pub fn is_success(&self) -> bool {
253        self.errors().next().is_none()
254    }
255
256    /// Returns true if there were errors during parsing and false if there were
257    /// not.
258    #[must_use]
259    pub fn is_failure(&self) -> bool {
260        !self.is_success()
261    }
262}
263
264impl Drop for ParseResult<'_> {
265    fn drop(&mut self) {
266        unsafe {
267            pm_parser_free(self.parser);
268            pm_arena_free(self.arena);
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use crate::parse;
276
277    #[test]
278    fn test_is_success() {
279        let result = parse(b"1 + 1");
280        assert!(result.is_success());
281        assert!(!result.is_failure());
282    }
283
284    #[test]
285    fn test_is_failure() {
286        let result = parse(b"<>");
287        assert!(result.is_failure());
288        assert!(!result.is_success());
289    }
290}