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(¤t_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 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(¤t_path), &path_str)?;
163
164 let normalized_content = normalize::source(&content);
165 let new_len = normalized_content.len();
166
167 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 source.replace_range(node_start..node_end, &content);
180 tree_source.replace_range(node_start..node_end, &normalized_content);
181
182 *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 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;