1use streaming_iterator::StreamingIterator;
20use tree_sitter::{Language, Parser, Query, QueryCursor};
21
22use crate::error::Error;
23use crate::types::{CodeLanguage, Section};
24
25pub fn parse_code(source: &str, lang: CodeLanguage) -> Result<Vec<Section>, Error> {
38 let ts_lang = language_for(lang);
39
40 let mut parser = Parser::new();
41 if let Err(e) = parser.set_language(&ts_lang) {
42 tracing::debug!(
46 lang = lang.as_str(),
47 error = %e,
48 "tree-sitter grammar ABI incompatible; falling back to full-file section"
49 );
50 return Ok(vec![Section {
51 heading: None,
52 depth: 0,
53 text: source.to_string(),
54 byte_range: 0..source.len(),
55 }]);
56 }
57
58 let tree = parser.parse(source, None).ok_or_else(|| Error::ParseFailed {
59 what: format!("code:{}", lang.as_str()),
60 detail: "tree-sitter parse returned None (input may be empty or cancelled)".into(),
61 })?;
62
63 let query_src = item_query(lang);
64 let query = match Query::new(&ts_lang, query_src) {
65 Ok(q) => q,
66 Err(e) => {
67 tracing::debug!(
68 lang = lang.as_str(),
69 error = %e,
70 "tree-sitter query compile failed; falling back to full-file section"
71 );
72 return Ok(vec![Section {
73 heading: None,
74 depth: 0,
75 text: source.to_string(),
76 byte_range: 0..source.len(),
77 }]);
78 }
79 };
80
81 let name_idx = query.capture_index_for_name("name");
82 let item_idx = query.capture_index_for_name("item");
83
84 let mut cursor = QueryCursor::new();
85 let raw = source.as_bytes();
86 let mut matches = cursor.matches(&query, tree.root_node(), raw);
87
88 let mut sections: Vec<Section> = Vec::new();
89
90 while let Some(m) = matches.next() {
91 let mut item_node = None;
92 let mut name_text: Option<String> = None;
93
94 for cap in m.captures {
95 if Some(cap.index) == item_idx {
96 item_node = Some(cap.node);
97 } else if Some(cap.index) == name_idx {
98 if let Ok(t) = cap.node.utf8_text(raw) {
99 name_text = Some(t.to_string());
100 }
101 }
102 }
103
104 let (Some(node), Some(name)) = (item_node, name_text) else {
105 continue;
106 };
107
108 let start_byte = node.start_byte();
109 let end_byte = node.end_byte();
110 let body = source[start_byte..end_byte].to_string();
111
112 let kind = item_kind_label(node.kind());
113 let heading = format!("{kind}:{name}");
114
115 sections.push(Section {
116 heading: Some(heading),
117 depth: 1,
118 text: body,
119 byte_range: start_byte..end_byte,
120 });
121 }
122
123 if sections.is_empty() {
124 return Ok(vec![Section {
126 heading: None,
127 depth: 0,
128 text: source.to_string(),
129 byte_range: 0..source.len(),
130 }]);
131 }
132
133 Ok(sections)
134}
135
136fn language_for(lang: CodeLanguage) -> Language {
139 match lang {
140 CodeLanguage::Rust => tree_sitter_rust::LANGUAGE.into(),
141 CodeLanguage::Python => tree_sitter_python::LANGUAGE.into(),
142 CodeLanguage::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
143 CodeLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
144 CodeLanguage::Go => tree_sitter_go::LANGUAGE.into(),
145 CodeLanguage::Java => tree_sitter_java::LANGUAGE.into(),
146 CodeLanguage::C => tree_sitter_c::LANGUAGE.into(),
147 CodeLanguage::Cpp => tree_sitter_cpp::LANGUAGE.into(),
148 CodeLanguage::Ruby => tree_sitter_ruby::LANGUAGE.into(),
149 CodeLanguage::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
150 }
151}
152
153fn item_query(lang: CodeLanguage) -> &'static str {
161 match lang {
162 CodeLanguage::Rust => concat!(
163 "(function_item name: (identifier) @name) @item\n",
164 "(struct_item name: (type_identifier) @name) @item\n",
165 "(enum_item name: (type_identifier) @name) @item\n",
166 "(trait_item name: (type_identifier) @name) @item\n",
167 ),
168 CodeLanguage::Python => concat!(
169 "(function_definition name: (identifier) @name) @item\n",
170 "(class_definition name: (identifier) @name) @item\n",
171 ),
172 CodeLanguage::JavaScript => concat!(
173 "(function_declaration name: (identifier) @name) @item\n",
174 "(class_declaration name: (identifier) @name) @item\n",
175 ),
176 CodeLanguage::TypeScript => concat!(
177 "(function_declaration name: (identifier) @name) @item\n",
178 "(class_declaration name: (type_identifier) @name) @item\n",
179 "(interface_declaration name: (type_identifier) @name) @item\n",
180 "(type_alias_declaration name: (type_identifier) @name) @item\n",
181 ),
182 CodeLanguage::Go => concat!(
183 "(function_declaration name: (identifier) @name) @item\n",
184 "(method_declaration name: (field_identifier) @name) @item\n",
185 "(type_spec name: (type_identifier) @name) @item\n",
186 ),
187 CodeLanguage::Java => concat!(
188 "(method_declaration name: (identifier) @name) @item\n",
189 "(class_declaration name: (identifier) @name) @item\n",
190 ),
191 CodeLanguage::C | CodeLanguage::Cpp => concat!(
192 "(function_definition\n",
193 " declarator: (function_declarator\n",
194 " declarator: (identifier) @name)) @item\n",
195 ),
196 CodeLanguage::Ruby => concat!(
197 "(method name: (identifier) @name) @item\n",
198 "(singleton_method name: (identifier) @name) @item\n",
199 "(class name: (constant) @name) @item\n",
200 "(module name: (constant) @name) @item\n",
201 ),
202 CodeLanguage::CSharp => concat!(
203 "(method_declaration name: (identifier) @name) @item\n",
204 "(class_declaration name: (identifier) @name) @item\n",
205 "(interface_declaration name: (identifier) @name) @item\n",
206 "(struct_declaration name: (identifier) @name) @item\n",
207 ),
208 }
209}
210
211fn item_kind_label(kind: &str) -> &'static str {
212 match kind {
213 "function_item" | "function_definition" | "function_declaration" => "fn",
214 "method_declaration" | "method_definition" | "method" | "singleton_method" => "method",
215 "struct_item" | "struct_declaration" => "struct",
216 "enum_item" => "enum",
217 "trait_item" => "trait",
218 "class_definition" | "class_declaration" => "class",
219 "interface_declaration" => "interface",
220 "module" => "module",
221 "type_alias_declaration" | "type_spec" => "type",
222 _ => "item",
223 }
224}
225
226#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn parse_rust_functions() {
234 let src = r#"
235fn add(a: i32, b: i32) -> i32 { a + b }
236
237struct Point { x: f32, y: f32 }
238
239fn main() {
240 println!("hello");
241}
242"#;
243 let sections = parse_code(src, CodeLanguage::Rust).unwrap();
244 assert!(
246 sections.len() >= 2,
247 "expected at least 2 sections, got {} - {:#?}",
248 sections.len(),
249 sections
250 );
251 let headings: Vec<_> = sections.iter().filter_map(|s| s.heading.as_deref()).collect();
252 assert!(
253 headings.iter().any(|h| h.starts_with("fn:add")),
254 "missing fn:add in {headings:?}"
255 );
256 assert!(
257 headings.iter().any(|h| h.starts_with("fn:main")),
258 "missing fn:main in {headings:?}"
259 );
260 }
261
262 #[test]
263 fn parse_python_class_and_function() {
264 let src = r#"
265class Animal:
266 def speak(self):
267 pass
268
269def standalone():
270 return 42
271"#;
272 let sections = parse_code(src, CodeLanguage::Python).unwrap();
273 let headings: Vec<_> = sections.iter().filter_map(|s| s.heading.as_deref()).collect();
274 assert!(
275 headings.iter().any(|h| h.starts_with("class:Animal")),
276 "missing class:Animal in {headings:?}"
277 );
278 assert!(
279 headings.iter().any(|h| h.starts_with("fn:standalone")),
280 "missing fn:standalone in {headings:?}"
281 );
282 }
283
284 #[test]
285 fn fallback_for_empty_or_no_items() {
286 let src = "// just a comment\n";
288 let sections = parse_code(src, CodeLanguage::Rust).unwrap();
289 assert_eq!(sections.len(), 1, "expected single fallback section");
290 assert!(sections[0].heading.is_none());
291 }
292
293 #[test]
294 fn section_byte_range_is_valid() {
295 let src = "fn foo() {}\nfn bar() {}\n";
296 let sections = parse_code(src, CodeLanguage::Rust).unwrap();
297 for s in §ions {
298 assert!(s.byte_range.end <= src.len());
299 assert!(s.byte_range.start <= s.byte_range.end);
300 }
301 }
302}