@@ -19,6 +19,8 @@ pub struct ParsedNode {
1919 /// Full parameter text from AST, e.g. "(a: number, b: string)" — includes names and types,
2020 /// not just type annotations. Stored as-is for FTS search (users may search by param names).
2121 pub param_types : Option < String > ,
22+ /// True if this node is inside a test context (#[cfg(test)], mod tests, #[test], etc.)
23+ pub is_test : bool ,
2224}
2325
2426thread_local ! {
@@ -53,32 +55,75 @@ pub fn parse_code(source: &str, language: &str) -> Result<Vec<ParsedNode>> {
5355/// Extract nodes from a pre-parsed tree (avoids re-parsing).
5456pub fn extract_nodes_from_tree ( tree : & tree_sitter:: Tree , source : & str , language : & str ) -> Vec < ParsedNode > {
5557 let mut nodes = Vec :: new ( ) ;
56- extract_nodes ( tree. root_node ( ) , source, language, None , & mut nodes, 0 ) ;
58+ extract_nodes ( tree. root_node ( ) , source, language, None , & mut nodes, 0 , false ) ;
5759 nodes
5860}
5961
62+ /// Check if a node has a preceding `#[cfg(test)]` or `#[test]` attribute.
63+ fn has_test_attribute ( node : & tree_sitter:: Node , source : & str ) -> bool {
64+ let mut sibling = node. prev_sibling ( ) ;
65+ while let Some ( s) = sibling {
66+ match s. kind ( ) {
67+ "attribute_item" | "inner_attribute_item" => {
68+ let text = node_text ( & s, source) ;
69+ if text. contains ( "cfg(test)" ) || text == "#[test]" {
70+ return true ;
71+ }
72+ }
73+ "line_comment" | "block_comment" | "comment" => { }
74+ _ => break ,
75+ }
76+ sibling = s. prev_sibling ( ) ;
77+ }
78+ false
79+ }
80+
6081fn extract_nodes (
6182 node : tree_sitter:: Node ,
6283 source : & str ,
6384 language : & str ,
6485 parent_class : Option < & str > ,
6586 results : & mut Vec < ParsedNode > ,
6687 depth : usize ,
88+ in_test_context : bool ,
6789) {
6890 if depth > MAX_AST_DEPTH { return ; }
6991 let kind = node. kind ( ) ;
7092
93+ // Detect Rust mod items (e.g., `mod tests { ... }`)
94+ if kind == "mod_item" {
95+ let mod_name = node. child_by_field_name ( "name" )
96+ . map ( |n| node_text ( & n, source) . to_string ( ) ) ;
97+ let is_test_mod = mod_name. as_deref ( ) == Some ( "tests" )
98+ || has_test_attribute ( & node, source) ;
99+ // Recurse into the module body with updated test context
100+ if let Some ( body) = node. child_by_field_name ( "body" ) {
101+ for i in 0 ..body. named_child_count ( ) {
102+ if let Some ( child) = body. named_child ( i) {
103+ extract_nodes ( child, source, language, parent_class, results, depth + 1 ,
104+ in_test_context || is_test_mod) ;
105+ }
106+ }
107+ }
108+ return ;
109+ }
110+
111+ // Check if this specific node has #[test] or #[cfg(test)]
112+ let node_is_test = in_test_context || has_test_attribute ( & node, source) ;
113+
71114 match kind {
72115 // Functions: shared across TS/JS/Go (function_declaration), Python/C/C++ (function_definition)
73116 "function_declaration" | "function" => {
74- if let Some ( parsed) = extract_function_node ( & node, source, "function" , parent_class) {
117+ if let Some ( mut parsed) = extract_function_node ( & node, source, "function" , parent_class) {
118+ parsed. is_test = node_is_test;
75119 results. push ( parsed) ;
76120 }
77121 }
78122 // Python async functions
79123 "async_function_definition" => {
80124 let nt = if parent_class. is_some ( ) { "method" } else { "function" } ;
81- if let Some ( parsed) = extract_function_node ( & node, source, nt, parent_class) {
125+ if let Some ( mut parsed) = extract_function_node ( & node, source, nt, parent_class) {
126+ parsed. is_test = node_is_test;
82127 results. push ( parsed) ;
83128 }
84129 }
@@ -100,27 +145,31 @@ fn extract_nodes(
100145 doc_comment : get_preceding_comment ( & node, source) ,
101146 return_type : sig_info. return_type ,
102147 param_types : sig_info. param_types ,
148+ is_test : node_is_test,
103149 } ) ;
104150 }
105151 }
106152 } else {
107153 // Python and others: name is in "name" field
108154 let nt = if parent_class. is_some ( ) { "method" } else { "function" } ;
109- if let Some ( parsed) = extract_function_node ( & node, source, nt, parent_class) {
155+ if let Some ( mut parsed) = extract_function_node ( & node, source, nt, parent_class) {
156+ parsed. is_test = node_is_test;
110157 results. push ( parsed) ;
111158 }
112159 }
113160 }
114161 "function_item" => {
115162 // Rust functions
116- if let Some ( parsed) = extract_function_node ( & node, source, "function" , parent_class) {
163+ if let Some ( mut parsed) = extract_function_node ( & node, source, "function" , parent_class) {
164+ parsed. is_test = node_is_test;
117165 results. push ( parsed) ;
118166 }
119167 }
120168
121169 // Arrow functions (TS/JS): covers const/let (lexical) and var (variable)
122170 "lexical_declaration" | "variable_declaration" => {
123- if let Some ( parsed) = extract_named_arrow ( & node, source) {
171+ if let Some ( mut parsed) = extract_named_arrow ( & node, source) {
172+ parsed. is_test = node_is_test;
124173 results. push ( parsed) ;
125174 }
126175 }
@@ -139,48 +188,50 @@ fn extract_nodes(
139188 doc_comment : get_preceding_comment ( & node, source) ,
140189 return_type : None ,
141190 param_types : None ,
191+ is_test : node_is_test,
142192 } ) ;
143- extract_children ( node, source, language, Some ( & name) , results, depth) ;
193+ extract_children ( node, source, language, Some ( & name) , results, depth, node_is_test ) ;
144194 return ;
145195 }
146196 }
147197
148198 // Methods: TS/JS (method_definition), Go/Java (method_declaration)
149199 "method_definition" | "method_declaration" => {
150- if let Some ( parsed) = extract_function_node ( & node, source, "method" , parent_class) {
200+ if let Some ( mut parsed) = extract_function_node ( & node, source, "method" , parent_class) {
201+ parsed. is_test = node_is_test;
151202 results. push ( parsed) ;
152203 }
153204 }
154205
155206 // Interfaces (TS/Java)
156207 "interface_declaration" => {
157208 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
158- results. push ( make_simple_node ( "interface" , name. clone ( ) , & node, source) ) ;
159- extract_children ( node, source, language, Some ( & name) , results, depth) ;
209+ results. push ( make_simple_node ( "interface" , name. clone ( ) , & node, source, node_is_test ) ) ;
210+ extract_children ( node, source, language, Some ( & name) , results, depth, node_is_test ) ;
160211 return ;
161212 }
162213 }
163214
164215 // TS type aliases: type Foo = ...
165216 "type_alias_declaration" => {
166217 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
167- results. push ( make_simple_node ( "type" , name, & node, source) ) ;
218+ results. push ( make_simple_node ( "type" , name, & node, source, node_is_test ) ) ;
168219 }
169220 }
170221
171222 // Java enums
172223 "enum_declaration" => {
173224 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
174- results. push ( make_simple_node ( "enum" , name, & node, source) ) ;
225+ results. push ( make_simple_node ( "enum" , name, & node, source, node_is_test ) ) ;
175226 }
176227 }
177228
178229 // C++ class/struct
179230 "class_specifier" | "struct_specifier" => {
180231 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
181232 let nt = if kind == "class_specifier" { "class" } else { "struct" } ;
182- results. push ( make_simple_node ( nt, name. clone ( ) , & node, source) ) ;
183- extract_children ( node, source, language, Some ( & name) , results, depth) ;
233+ results. push ( make_simple_node ( nt, name. clone ( ) , & node, source, node_is_test ) ) ;
234+ extract_children ( node, source, language, Some ( & name) , results, depth, node_is_test ) ;
184235 return ;
185236 }
186237 }
@@ -211,6 +262,7 @@ fn extract_nodes(
211262 doc_comment : get_preceding_comment ( & child, source) ,
212263 return_type : None ,
213264 param_types : None ,
265+ is_test : node_is_test,
214266 } ) ;
215267 }
216268 }
@@ -221,25 +273,25 @@ fn extract_nodes(
221273 // Rust-specific
222274 "struct_item" => {
223275 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
224- results. push ( make_simple_node ( "struct" , name, & node, source) ) ;
276+ results. push ( make_simple_node ( "struct" , name, & node, source, node_is_test ) ) ;
225277 }
226278 }
227279 "enum_item" => {
228280 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
229- results. push ( make_simple_node ( "enum" , name, & node, source) ) ;
281+ results. push ( make_simple_node ( "enum" , name, & node, source, node_is_test ) ) ;
230282 }
231283 }
232284 "impl_item" => {
233285 if let Some ( type_node) = node. child_by_field_name ( "type" ) {
234286 let impl_name = node_text ( & type_node, source) ;
235- extract_children ( node, source, language, Some ( impl_name) , results, depth) ;
287+ extract_children ( node, source, language, Some ( impl_name) , results, depth, node_is_test ) ;
236288 return ;
237289 }
238290 }
239291 "trait_item" => {
240292 if let Some ( name) = get_child_by_field ( & node, "name" , source) {
241- results. push ( make_simple_node ( "interface" , name. clone ( ) , & node, source) ) ;
242- extract_children ( node, source, language, Some ( & name) , results, depth) ;
293+ results. push ( make_simple_node ( "interface" , name. clone ( ) , & node, source, node_is_test ) ) ;
294+ extract_children ( node, source, language, Some ( & name) , results, depth, node_is_test ) ;
243295 return ;
244296 }
245297 }
@@ -248,7 +300,7 @@ fn extract_nodes(
248300 }
249301
250302 // Recurse into children
251- extract_children ( node, source, language, parent_class, results, depth) ;
303+ extract_children ( node, source, language, parent_class, results, depth, node_is_test ) ;
252304}
253305
254306fn extract_children (
@@ -258,10 +310,11 @@ fn extract_children(
258310 parent_class : Option < & str > ,
259311 results : & mut Vec < ParsedNode > ,
260312 depth : usize ,
313+ in_test_context : bool ,
261314) {
262315 for i in 0 ..node. named_child_count ( ) {
263316 if let Some ( child) = node. named_child ( i) {
264- extract_nodes ( child, source, language, parent_class, results, depth + 1 ) ;
317+ extract_nodes ( child, source, language, parent_class, results, depth + 1 , in_test_context ) ;
265318 }
266319 }
267320}
@@ -280,7 +333,7 @@ fn truncate_code_content(content: &str) -> Cow<'_, str> {
280333 }
281334}
282335
283- fn make_simple_node ( node_type : & str , name : String , node : & tree_sitter:: Node , source : & str ) -> ParsedNode {
336+ fn make_simple_node ( node_type : & str , name : String , node : & tree_sitter:: Node , source : & str , is_test : bool ) -> ParsedNode {
284337 ParsedNode {
285338 node_type : node_type. into ( ) ,
286339 name : name. clone ( ) ,
@@ -292,6 +345,7 @@ fn make_simple_node(node_type: &str, name: String, node: &tree_sitter::Node, sou
292345 doc_comment : get_preceding_comment ( node, source) ,
293346 return_type : None ,
294347 param_types : None ,
348+ is_test,
295349 }
296350}
297351
@@ -319,6 +373,7 @@ fn extract_function_node(
319373 doc_comment : get_preceding_comment ( node, source) ,
320374 return_type : sig_info. return_type ,
321375 param_types : sig_info. param_types ,
376+ is_test : false ,
322377 } )
323378}
324379
@@ -345,6 +400,7 @@ fn extract_named_arrow(node: &tree_sitter::Node, source: &str) -> Option<ParsedN
345400 doc_comment : get_preceding_comment ( node, source) ,
346401 return_type : sig_info. return_type ,
347402 param_types : sig_info. param_types ,
403+ is_test : false ,
348404 } ) ;
349405 }
350406 }
0 commit comments