Skip to main content

xidl_parser/parser/
mod.rs

1mod doc_comments;
2mod normalize;
3pub(crate) mod raw;
4
5use crate::error::ParserResult;
6use std::collections::{HashMap, HashSet};
7use tree_sitter::{InputEdit, Node, Parser, Point, Tree};
8
9pub struct ParseContext<'a> {
10    pub source: &'a [u8],
11    pub symbols: HashMap<String, String>,
12    doc_consumed: HashSet<usize>,
13}
14
15pub trait IncludeResolver {
16    fn resolve(&mut self, parent_path: Option<&str>, path: &str) -> ParserResult<(String, String)>;
17}
18
19struct NoopIncludeResolver;
20impl IncludeResolver for NoopIncludeResolver {
21    fn resolve(
22        &mut self,
23        _parent_path: Option<&str>,
24        _path: &str,
25    ) -> ParserResult<(String, String)> {
26        Err(crate::error::ParseError::Message(
27            "Include resolution not supported in this context".to_string(),
28        ))
29    }
30}
31
32impl<'a> ParseContext<'a> {
33    pub fn new(source: &'a [u8]) -> Self {
34        Self {
35            source,
36            symbols: HashMap::new(),
37            doc_consumed: HashSet::new(),
38        }
39    }
40
41    pub fn node_text(&self, node: &Node) -> ParserResult<&str> {
42        Ok(node.utf8_text(self.source)?)
43    }
44
45    pub fn take_doc_comment(&mut self, node: &Node) -> Option<String> {
46        let start = node.start_byte();
47        if self.doc_consumed.contains(&start) {
48            return None;
49        }
50        let doc = doc_comments::extract(self.source, start);
51        if doc.is_some() {
52            self.doc_consumed.insert(start);
53        }
54        doc
55    }
56}
57
58pub trait FromTreeSitter<'a>: Sized {
59    fn from_node(node: Node<'a>, context: &mut ParseContext<'a>) -> ParserResult<Self>;
60}
61
62impl<'a> FromTreeSitter<'a> for String {
63    fn from_node(node: Node<'a>, context: &mut ParseContext<'a>) -> ParserResult<Self> {
64        Ok(context.node_text(&node)?.to_string())
65    }
66}
67
68impl<'a, T> FromTreeSitter<'a> for Box<T>
69where
70    T: FromTreeSitter<'a>,
71{
72    fn from_node(node: Node<'a>, context: &mut ParseContext<'a>) -> ParserResult<Self> {
73        Ok(Box::new(T::from_node(node, context)?))
74    }
75}
76
77pub fn parser_text(text: &str) -> ParserResult<crate::typed_ast::Specification> {
78    parser_text_with_resolver(text, None, &mut NoopIncludeResolver)
79}
80
81pub fn parser_text_with_resolver(
82    text: &str,
83    initial_path: Option<&str>,
84    resolver: &mut dyn IncludeResolver,
85) -> ParserResult<crate::typed_ast::Specification> {
86    use crate::typed_ast::Specification;
87
88    let mut parser = tree_sitter::Parser::new();
89    parser.set_language(&tree_sitter_idl::language()).unwrap();
90
91    let mut source = text.to_string();
92    let mut tree_source = normalize::source(text).into_owned();
93
94    let mut tree = parser.parse(&tree_source, None).ok_or_else(|| {
95        crate::error::ParseError::TreeSitterError("Failed to parse text".to_string())
96    })?;
97
98    if let Some(path) = initial_path {
99        let mut include_stack = Vec::new();
100        let source_len = tree_source.len();
101        expand_includes(
102            &mut source,
103            &mut tree_source,
104            &mut tree,
105            &mut parser,
106            resolver,
107            &mut include_stack,
108            path.to_string(),
109            0,
110            source_len,
111        )?;
112    }
113
114    let root_node = tree.root_node();
115    if root_node.has_error() {
116        return Err(crate::error::ParseError::TreeSitterError(
117            "Failed to parse text".to_string(),
118        ));
119    }
120    let mut context = ParseContext::new(source.as_bytes());
121
122    Specification::from_node(root_node, &mut context)
123}
124
125#[allow(clippy::too_many_arguments)]
126fn expand_includes(
127    source: &mut String,
128    tree_source: &mut String,
129    tree: &mut Tree,
130    parser: &mut Parser,
131    resolver: &mut dyn IncludeResolver,
132    include_stack: &mut Vec<String>,
133    current_path: String,
134    start_offset: usize,
135    mut end_offset: usize,
136) -> ParserResult<usize> {
137    if include_stack.contains(&current_path) {
138        let chain = include_stack.join(" -> ");
139        return Err(crate::error::ParseError::Message(format!(
140            "cyclic include detected: {} -> {}",
141            chain, current_path
142        )));
143    }
144    include_stack.push(current_path.clone());
145
146    let mut search_start = start_offset;
147    while let Some(node) = find_first_include_in_range(tree.root_node(), search_start, end_offset) {
148        let node_start = node.start_byte();
149        let node_end = node.end_byte();
150        let node_start_pos = node.start_position();
151        let node_end_pos = node.end_position();
152
153        let path_str = extract_include_path(node, tree_source)?;
154
155        // A versioned include is spliced while the HIR is lowered: the version
156        // projection only exists there, so the directive stays in the tree.
157        if crate::hir::split_version_selector(&path_str).1.is_some() {
158            search_start = node_end;
159            continue;
160        }
161
162        let (actual_path, content) = resolver.resolve(Some(&current_path), &path_str)?;
163
164        let normalized_content = normalize::source(&content);
165        let new_len = normalized_content.len();
166
167        // Edit tree
168        let edit = InputEdit {
169            start_byte: node_start,
170            old_end_byte: node_end,
171            new_end_byte: node_start + new_len,
172            start_position: node_start_pos,
173            old_end_position: node_end_pos,
174            new_end_position: get_pos_after(node_start_pos, &normalized_content),
175        };
176        tree.edit(&edit);
177
178        // Update source and tree_source
179        source.replace_range(node_start..node_end, &content);
180        tree_source.replace_range(node_start..node_end, &normalized_content);
181
182        // Re-parse
183        *tree = parser.parse(&*tree_source, Some(tree)).ok_or_else(|| {
184            crate::error::ParseError::TreeSitterError("Failed to re-parse text".to_string())
185        })?;
186
187        if tree.root_node().has_error()
188            && has_error_in_range(tree.root_node(), node_start, node_start + new_len)
189        {
190            return Err(crate::error::ParseError::Message(format!(
191                "failed to parse include '{}'",
192                path_str
193            )));
194        }
195
196        // Recurse
197        let expanded_len = expand_includes(
198            source,
199            tree_source,
200            tree,
201            parser,
202            resolver,
203            include_stack,
204            actual_path,
205            node_start,
206            node_start + new_len,
207        )?;
208
209        let diff = (expanded_len as isize) - ((node_end - node_start) as isize);
210        end_offset = ((end_offset as isize) + diff) as usize;
211        search_start = node_start + expanded_len;
212    }
213
214    include_stack.pop();
215    Ok(end_offset - start_offset)
216}
217
218fn find_first_include_in_range(node: Node, start: usize, end: usize) -> Option<Node> {
219    if node.start_byte() >= end || node.end_byte() <= start {
220        return None;
221    }
222
223    if node.kind() == "preproc_include" && node.start_byte() >= start && node.end_byte() <= end {
224        return Some(node);
225    }
226
227    let mut cursor = node.walk();
228    for child in node.children(&mut cursor) {
229        if let Some(found) = find_first_include_in_range(child, start, end) {
230            return Some(found);
231        }
232    }
233    None
234}
235
236fn has_error_in_range(node: Node, start: usize, end: usize) -> bool {
237    if node.start_byte() >= end || node.end_byte() <= start {
238        return false;
239    }
240    if node.is_error() || node.is_missing() {
241        return true;
242    }
243    let mut cursor = node.walk();
244    for child in node.children(&mut cursor) {
245        if has_error_in_range(child, start, end) {
246            return true;
247        }
248    }
249    false
250}
251
252fn get_pos_after(start: Point, text: &str) -> Point {
253    let mut row = start.row;
254    let mut col = start.column;
255    for b in text.as_bytes() {
256        if *b == b'\n' {
257            row += 1;
258            col = 0;
259        } else {
260            col += 1;
261        }
262    }
263    Point { row, column: col }
264}
265
266fn extract_include_path(node: Node, source: &str) -> ParserResult<String> {
267    let mut cursor = node.walk();
268    for child in node.children(&mut cursor) {
269        match child.kind() {
270            "string_literal" => {
271                let text = child.utf8_text(source.as_bytes())?;
272                return Ok(text
273                    .trim_matches(|c| c == '"' || c == '<' || c == '>')
274                    .to_string());
275            }
276            "system_lib_string" => {
277                let text = child.utf8_text(source.as_bytes())?;
278                return Err(crate::error::ParseError::Message(format!(
279                    "unsupported include path syntax {}; only string literal includes are supported",
280                    text
281                )));
282            }
283            "identifier" => {
284                let text = child.utf8_text(source.as_bytes())?;
285                return Err(crate::error::ParseError::Message(format!(
286                    "unsupported include identifier '{}'; only string literal includes are supported",
287                    text
288                )));
289            }
290            _ => {}
291        }
292    }
293    Err(crate::error::ParseError::UnexpectedNode(
294        "missing include path".to_string(),
295    ))
296}
297
298pub fn normalize_source_for_tree_sitter(text: &str) -> std::borrow::Cow<'_, str> {
299    normalize::source(text)
300}
301
302#[cfg(test)]
303mod tests;