Skip to main content

reflex/parsers/
c.rs

1//! C language parser using Tree-sitter
2//!
3//! Extracts symbols from C source code:
4//! - Functions (declarations and definitions)
5//! - Structs
6//! - Enums
7//! - Unions
8//! - Typedefs
9//! - Variables (global, local, static, extern)
10//! - Macros (#define for function-like and constant macros)
11
12use crate::models::{Language, SearchResult, Span, SymbolKind};
13use anyhow::{Context, Result};
14use streaming_iterator::StreamingIterator;
15use tree_sitter::{Parser, Query, QueryCursor};
16
17/// Parse C source code and extract symbols
18pub fn parse(path: &str, source: &str) -> Result<Vec<SearchResult>> {
19    let mut parser = Parser::new();
20    let language = tree_sitter_c::LANGUAGE;
21
22    parser
23        .set_language(&language.into())
24        .context("Failed to set C language")?;
25
26    let tree = parser
27        .parse(source, None)
28        .context("Failed to parse C source")?;
29
30    let root_node = tree.root_node();
31
32    let mut symbols = Vec::new();
33
34    // Extract different types of symbols using Tree-sitter queries
35    symbols.extend(extract_functions(source, &root_node, &language.into())?);
36    symbols.extend(extract_structs(source, &root_node, &language.into())?);
37    symbols.extend(extract_enums(source, &root_node, &language.into())?);
38    symbols.extend(extract_unions(source, &root_node, &language.into())?);
39    symbols.extend(extract_typedefs(source, &root_node, &language.into())?);
40    symbols.extend(extract_variables(source, &root_node, &language.into())?);
41    symbols.extend(extract_macros(source, &root_node, &language.into())?);
42
43    // Add file path to all symbols
44    for symbol in &mut symbols {
45        symbol.path = path.to_string();
46        symbol.lang = Language::C;
47    }
48
49    Ok(symbols)
50}
51
52/// Extract function declarations and definitions
53fn extract_functions(
54    source: &str,
55    root: &tree_sitter::Node,
56    language: &tree_sitter::Language,
57) -> Result<Vec<SearchResult>> {
58    let query_str = r#"
59        (function_definition
60            declarator: (function_declarator
61                declarator: (identifier) @name)) @function
62
63        (function_definition
64            declarator: (pointer_declarator
65                declarator: (function_declarator
66                    declarator: (identifier) @name))) @function
67    "#;
68
69    let query = Query::new(language, query_str).context("Failed to create function query")?;
70
71    extract_symbols(source, root, &query, SymbolKind::Function, None)
72}
73
74/// Extract struct definitions
75fn extract_structs(
76    source: &str,
77    root: &tree_sitter::Node,
78    language: &tree_sitter::Language,
79) -> Result<Vec<SearchResult>> {
80    let query_str = r#"
81        (struct_specifier
82            name: (type_identifier) @name) @struct
83    "#;
84
85    let query = Query::new(language, query_str).context("Failed to create struct query")?;
86
87    extract_symbols(source, root, &query, SymbolKind::Struct, None)
88}
89
90/// Extract enum definitions
91fn extract_enums(
92    source: &str,
93    root: &tree_sitter::Node,
94    language: &tree_sitter::Language,
95) -> Result<Vec<SearchResult>> {
96    let query_str = r#"
97        (enum_specifier
98            name: (type_identifier) @name) @enum
99    "#;
100
101    let query = Query::new(language, query_str).context("Failed to create enum query")?;
102
103    extract_symbols(source, root, &query, SymbolKind::Enum, None)
104}
105
106/// Extract union definitions
107fn extract_unions(
108    source: &str,
109    root: &tree_sitter::Node,
110    language: &tree_sitter::Language,
111) -> Result<Vec<SearchResult>> {
112    let query_str = r#"
113        (union_specifier
114            name: (type_identifier) @name) @union
115    "#;
116
117    let query = Query::new(language, query_str).context("Failed to create union query")?;
118
119    extract_symbols(source, root, &query, SymbolKind::Type, None)
120}
121
122/// Extract typedef declarations
123fn extract_typedefs(
124    source: &str,
125    root: &tree_sitter::Node,
126    language: &tree_sitter::Language,
127) -> Result<Vec<SearchResult>> {
128    let query_str = r#"
129        (type_definition
130            declarator: (type_identifier) @name) @typedef
131    "#;
132
133    let query = Query::new(language, query_str).context("Failed to create typedef query")?;
134
135    extract_symbols(source, root, &query, SymbolKind::Type, None)
136}
137
138/// Extract variable declarations (global and local)
139fn extract_variables(
140    source: &str,
141    root: &tree_sitter::Node,
142    language: &tree_sitter::Language,
143) -> Result<Vec<SearchResult>> {
144    let query_str = r#"
145        (declaration
146            declarator: (init_declarator
147                declarator: (identifier) @name)) @var
148
149        (declaration
150            declarator: (identifier) @name) @var
151    "#;
152
153    let query = Query::new(language, query_str).context("Failed to create variable query")?;
154
155    // Extract all variable declarations (global and local)
156    let mut cursor = QueryCursor::new();
157    let mut matches = cursor.matches(&query, *root, source.as_bytes());
158
159    let mut symbols = Vec::new();
160
161    while let Some(match_) = matches.next() {
162        let mut name = None;
163        let mut var_node = None;
164
165        for capture in match_.captures {
166            let capture_name: &str = query.capture_names()[capture.index as usize];
167            if capture_name == "name" {
168                name = Some(
169                    capture
170                        .node
171                        .utf8_text(source.as_bytes())
172                        .unwrap_or("")
173                        .to_string(),
174                );
175            } else if capture_name == "var" {
176                var_node = Some(capture.node);
177            }
178        }
179
180        if let (Some(name), Some(node)) = (name, var_node) {
181            let span = node_to_span(&node);
182            let preview = extract_preview(source, &span);
183
184            symbols.push(SearchResult::new(
185                String::new(),
186                Language::C,
187                SymbolKind::Variable,
188                Some(name),
189                span,
190                None,
191                preview,
192            ));
193        }
194    }
195
196    Ok(symbols)
197}
198
199/// Extract macro definitions (#define)
200fn extract_macros(
201    source: &str,
202    root: &tree_sitter::Node,
203    language: &tree_sitter::Language,
204) -> Result<Vec<SearchResult>> {
205    let query_str = r#"
206        (preproc_def
207            name: (identifier) @name) @macro
208
209        (preproc_function_def
210            name: (identifier) @name) @macro
211    "#;
212
213    let query = Query::new(language, query_str).context("Failed to create macro query")?;
214
215    extract_symbols(source, root, &query, SymbolKind::Macro, None)
216}
217
218/// Generic symbol extraction helper
219fn extract_symbols(
220    source: &str,
221    root: &tree_sitter::Node,
222    query: &Query,
223    kind: SymbolKind,
224    scope: Option<String>,
225) -> Result<Vec<SearchResult>> {
226    let mut cursor = QueryCursor::new();
227    let mut matches = cursor.matches(query, *root, source.as_bytes());
228
229    let mut symbols = Vec::new();
230
231    while let Some(match_) = matches.next() {
232        // Find the name capture and the full node
233        let mut name = None;
234        let mut full_node = None;
235
236        for capture in match_.captures {
237            let capture_name: &str = query.capture_names()[capture.index as usize];
238            if capture_name == "name" {
239                name = Some(
240                    capture
241                        .node
242                        .utf8_text(source.as_bytes())
243                        .unwrap_or("")
244                        .to_string(),
245                );
246            } else {
247                // Assume any other capture is the full node
248                full_node = Some(capture.node);
249            }
250        }
251
252        if let (Some(name), Some(node)) = (name, full_node) {
253            let span = node_to_span(&node);
254            let preview = extract_preview(source, &span);
255
256            symbols.push(SearchResult::new(
257                String::new(),
258                Language::C,
259                kind.clone(),
260                Some(name),
261                span,
262                scope.clone(),
263                preview,
264            ));
265        }
266    }
267
268    Ok(symbols)
269}
270
271/// Convert a Tree-sitter node to a Span
272fn node_to_span(node: &tree_sitter::Node) -> Span {
273    let start = node.start_position();
274    let end = node.end_position();
275
276    Span::new(
277        start.row + 1, // Convert 0-indexed to 1-indexed
278        start.column,
279        end.row + 1,
280        end.column,
281    )
282}
283
284/// Extract a preview (7 lines) around the symbol
285fn extract_preview(source: &str, span: &Span) -> String {
286    let lines: Vec<&str> = source.lines().collect();
287
288    // Extract 7 lines: the start line and 6 following lines
289    let start_idx = span.start_line - 1; // Convert back to 0-indexed
290    let end_idx = (start_idx + 7).min(lines.len());
291
292    lines[start_idx..end_idx].join("\n")
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_parse_function() {
301        let source = r#"
302int add(int a, int b) {
303    return a + b;
304}
305        "#;
306
307        let symbols = parse("test.c", source).unwrap();
308        assert_eq!(symbols.len(), 1);
309        assert_eq!(symbols[0].symbol.as_deref(), Some("add"));
310        assert!(matches!(symbols[0].kind, SymbolKind::Function));
311    }
312
313    #[test]
314    fn test_parse_struct() {
315        let source = r#"
316struct User {
317    char name[50];
318    int age;
319};
320        "#;
321
322        let symbols = parse("test.c", source).unwrap();
323        assert_eq!(symbols.len(), 1);
324        assert_eq!(symbols[0].symbol.as_deref(), Some("User"));
325        assert!(matches!(symbols[0].kind, SymbolKind::Struct));
326    }
327
328    #[test]
329    fn test_parse_enum() {
330        let source = r#"
331enum Status {
332    STATUS_ACTIVE,
333    STATUS_INACTIVE,
334    STATUS_PENDING
335};
336        "#;
337
338        let symbols = parse("test.c", source).unwrap();
339        assert_eq!(symbols.len(), 1);
340        assert_eq!(symbols[0].symbol.as_deref(), Some("Status"));
341        assert!(matches!(symbols[0].kind, SymbolKind::Enum));
342    }
343
344    #[test]
345    fn test_parse_typedef() {
346        let source = r#"
347typedef struct {
348    int x;
349    int y;
350} Point;
351
352typedef int UserID;
353        "#;
354
355        let symbols = parse("test.c", source).unwrap();
356
357        let typedef_symbols: Vec<_> = symbols
358            .iter()
359            .filter(|s| matches!(s.kind, SymbolKind::Type))
360            .collect();
361
362        assert!(!typedef_symbols.is_empty());
363        assert!(
364            typedef_symbols
365                .iter()
366                .any(|s| s.symbol.as_deref() == Some("Point"))
367        );
368    }
369
370    #[test]
371    fn test_parse_union() {
372        let source = r#"
373union Data {
374    int i;
375    float f;
376    char str[20];
377};
378        "#;
379
380        let symbols = parse("test.c", source).unwrap();
381
382        let union_symbols: Vec<_> = symbols
383            .iter()
384            .filter(|s| matches!(s.kind, SymbolKind::Type))
385            .collect();
386
387        assert_eq!(union_symbols.len(), 1);
388        assert_eq!(union_symbols[0].symbol.as_deref(), Some("Data"));
389    }
390
391    #[test]
392    fn test_parse_global_variables() {
393        let source = r#"
394int global_counter = 0;
395static int internal_state;
396extern int external_value;
397        "#;
398
399        let symbols = parse("test.c", source).unwrap();
400
401        let var_symbols: Vec<_> = symbols
402            .iter()
403            .filter(|s| matches!(s.kind, SymbolKind::Variable))
404            .collect();
405
406        assert_eq!(var_symbols.len(), 3);
407        assert!(
408            var_symbols
409                .iter()
410                .any(|s| s.symbol.as_deref() == Some("global_counter"))
411        );
412        assert!(
413            var_symbols
414                .iter()
415                .any(|s| s.symbol.as_deref() == Some("internal_state"))
416        );
417        assert!(
418            var_symbols
419                .iter()
420                .any(|s| s.symbol.as_deref() == Some("external_value"))
421        );
422    }
423
424    #[test]
425    fn test_parse_pointer_function() {
426        let source = r#"
427int* create_array(int size) {
428    return malloc(size * sizeof(int));
429}
430        "#;
431
432        let symbols = parse("test.c", source).unwrap();
433        assert_eq!(symbols.len(), 1);
434        assert_eq!(symbols[0].symbol.as_deref(), Some("create_array"));
435        assert!(matches!(symbols[0].kind, SymbolKind::Function));
436    }
437
438    #[test]
439    fn test_parse_mixed_symbols() {
440        let source = r#"
441#include <stdio.h>
442
443#define MAX_SIZE 100
444
445typedef struct {
446    char name[50];
447    int age;
448} Person;
449
450enum Color {
451    RED,
452    GREEN,
453    BLUE
454};
455
456int global_count = 0;
457
458int increment(void) {
459    return ++global_count;
460}
461
462struct Node {
463    int data;
464    struct Node* next;
465};
466        "#;
467
468        let symbols = parse("test.c", source).unwrap();
469
470        // Should find: macro, typedef, enum, variable, function, struct
471        assert!(symbols.len() >= 6);
472
473        let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
474        assert!(kinds.contains(&&SymbolKind::Macro));
475        assert!(kinds.contains(&&SymbolKind::Type));
476        assert!(kinds.contains(&&SymbolKind::Enum));
477        assert!(kinds.contains(&&SymbolKind::Variable));
478        assert!(kinds.contains(&&SymbolKind::Function));
479        assert!(kinds.contains(&&SymbolKind::Struct));
480
481        // Verify the macro symbol is found
482        let macro_symbols: Vec<_> = symbols
483            .iter()
484            .filter(|s| matches!(s.kind, SymbolKind::Macro))
485            .collect();
486        assert_eq!(macro_symbols.len(), 1);
487        assert_eq!(macro_symbols[0].symbol.as_deref(), Some("MAX_SIZE"));
488    }
489
490    #[test]
491    fn test_parse_struct_with_typedef() {
492        let source = r#"
493typedef struct Node {
494    int value;
495    struct Node* next;
496} Node;
497        "#;
498
499        let symbols = parse("test.c", source).unwrap();
500
501        // Should find both the struct and the typedef
502        assert!(!symbols.is_empty());
503        assert!(symbols.iter().any(|s| s.symbol.as_deref() == Some("Node")));
504    }
505
506    #[test]
507    fn test_local_variables_included() {
508        let source = r#"
509int global_var = 10;
510
511int calculate(int x) {
512    int local_var = x * 2;
513    return local_var;
514}
515        "#;
516
517        let symbols = parse("test.c", source).unwrap();
518
519        let var_symbols: Vec<_> = symbols
520            .iter()
521            .filter(|s| matches!(s.kind, SymbolKind::Variable))
522            .collect();
523
524        // Should find both global_var and local_var
525        assert_eq!(var_symbols.len(), 2);
526        assert!(
527            var_symbols
528                .iter()
529                .any(|s| s.symbol.as_deref() == Some("global_var"))
530        );
531        assert!(
532            var_symbols
533                .iter()
534                .any(|s| s.symbol.as_deref() == Some("local_var"))
535        );
536    }
537
538    #[test]
539    fn test_parse_macros() {
540        let source = r#"
541#define MAX_SIZE 100
542#define MIN(a, b) ((a) < (b) ? (a) : (b))
543#define DEBUG_PRINT(x) printf("Debug: %s\n", x)
544
545int main() {
546    return 0;
547}
548        "#;
549
550        let symbols = parse("test.c", source).unwrap();
551
552        let macro_symbols: Vec<_> = symbols
553            .iter()
554            .filter(|s| matches!(s.kind, SymbolKind::Macro))
555            .collect();
556
557        // Should find all three macros
558        assert_eq!(macro_symbols.len(), 3);
559        assert!(
560            macro_symbols
561                .iter()
562                .any(|s| s.symbol.as_deref() == Some("MAX_SIZE"))
563        );
564        assert!(
565            macro_symbols
566                .iter()
567                .any(|s| s.symbol.as_deref() == Some("MIN"))
568        );
569        assert!(
570            macro_symbols
571                .iter()
572                .any(|s| s.symbol.as_deref() == Some("DEBUG_PRINT"))
573        );
574    }
575}
576
577// ============================================================================
578// Dependency Extraction
579// ============================================================================
580
581use crate::models::ImportType;
582use crate::parsers::{DependencyExtractor, ImportInfo};
583
584/// C dependency extractor
585pub struct CDependencyExtractor;
586
587impl DependencyExtractor for CDependencyExtractor {
588    fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
589        let mut parser = Parser::new();
590        let language = tree_sitter_c::LANGUAGE;
591
592        parser
593            .set_language(&language.into())
594            .context("Failed to set C language")?;
595
596        let tree = parser
597            .parse(source, None)
598            .context("Failed to parse C source")?;
599
600        let root_node = tree.root_node();
601
602        let mut imports = Vec::new();
603
604        // Extract #include directives
605        imports.extend(extract_c_includes(source, &root_node)?);
606
607        Ok(imports)
608    }
609}
610
611/// Extract C #include directives
612fn extract_c_includes(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
613    let language = tree_sitter_c::LANGUAGE;
614
615    let query_str = r#"
616        (preproc_include
617            path: (string_literal) @include_path) @include
618
619        (preproc_include
620            path: (system_lib_string) @include_path) @include
621    "#;
622
623    let query =
624        Query::new(&language.into(), query_str).context("Failed to create C include query")?;
625
626    let mut cursor = QueryCursor::new();
627    let mut matches = cursor.matches(&query, *root, source.as_bytes());
628
629    let mut imports = Vec::new();
630
631    while let Some(match_) = matches.next() {
632        let mut include_path = None;
633        let mut include_node = None;
634
635        for capture in match_.captures {
636            let capture_name: &str = query.capture_names()[capture.index as usize];
637            match capture_name {
638                "include_path" => {
639                    // Remove quotes or angle brackets from path
640                    let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
641                    include_path = Some(
642                        raw_path
643                            .trim_matches(|c| c == '"' || c == '<' || c == '>')
644                            .to_string(),
645                    );
646                }
647                "include" => {
648                    include_node = Some(capture.node);
649                }
650                _ => {}
651            }
652        }
653
654        if let (Some(path), Some(node)) = (include_path, include_node) {
655            let import_type = classify_c_include(&path, source, &node);
656            let line_number = node.start_position().row + 1;
657
658            imports.push(ImportInfo {
659                imported_path: path,
660                import_type,
661                line_number,
662                imported_symbols: None, // C includes entire header
663            });
664        }
665    }
666
667    Ok(imports)
668}
669
670/// Classify a C include as internal, external, or stdlib
671fn classify_c_include(include_path: &str, source: &str, node: &tree_sitter::Node) -> ImportType {
672    // Get the actual #include line to check if it uses quotes or angle brackets
673    let line_start = node.start_position();
674    let lines: Vec<&str> = source.lines().collect();
675
676    if line_start.row < lines.len() {
677        let line = lines[line_start.row];
678
679        // Internal: #include "..." (quotes = local project files)
680        if line.contains(&format!("\"{}\"", include_path)) {
681            return ImportType::Internal;
682        }
683    }
684
685    // C standard library headers (angle brackets)
686    const STDLIB_HEADERS: &[&str] = &[
687        "stdio.h",
688        "stdlib.h",
689        "string.h",
690        "math.h",
691        "time.h",
692        "ctype.h",
693        "assert.h",
694        "errno.h",
695        "limits.h",
696        "float.h",
697        "stddef.h",
698        "stdint.h",
699        "stdbool.h",
700        "stdarg.h",
701        "setjmp.h",
702        "signal.h",
703        "locale.h",
704        "wchar.h",
705        "wctype.h",
706        "complex.h",
707        "fenv.h",
708        "inttypes.h",
709        "iso646.h",
710        "tgmath.h",
711        "threads.h",
712    ];
713
714    if STDLIB_HEADERS.contains(&include_path) {
715        return ImportType::Stdlib;
716    }
717
718    // Everything else with angle brackets is external (third-party libraries)
719    ImportType::External
720}
721
722// ============================================================================
723// Path Resolution
724// ============================================================================
725
726/// Resolve a C #include directive to a file path
727///
728/// # Arguments
729/// * `include_path` - The path from the #include directive (e.g., "utils/helper.h")
730/// * `current_file_path` - Path to the file containing the #include directive
731///
732/// # Returns
733/// * `Some(path)` if the include can be resolved (quoted includes only)
734/// * `None` for angle bracket includes (system/library headers)
735pub fn resolve_c_include_to_path(
736    include_path: &str,
737    current_file_path: Option<&str>,
738) -> Option<String> {
739    // Only resolve relative includes (quoted includes, which are Internal)
740    // Angle bracket includes are system/library headers and won't be resolved
741
742    let current_file = current_file_path?;
743
744    // Get directory of current file
745    let current_dir = std::path::Path::new(current_file).parent()?;
746
747    // Resolve the include path relative to current file
748    let resolved = current_dir.join(include_path);
749
750    // Normalize the path. Always emit forward slashes so resolved paths are
751    // deterministic across platforms.
752    match resolved.canonicalize() {
753        Ok(normalized) => Some(normalized.to_string_lossy().replace('\\', "/")),
754        Err(_) => {
755            // If canonicalize fails (file doesn't exist yet), return the joined path
756            Some(resolved.to_string_lossy().replace('\\', "/"))
757        }
758    }
759}
760
761// ============================================================================
762// Tests for Path Resolution
763// ============================================================================
764
765#[cfg(test)]
766mod resolution_tests {
767    use super::*;
768
769    #[test]
770    fn test_resolve_c_include_same_directory() {
771        let result = resolve_c_include_to_path("helper.h", Some("/project/src/main.c"));
772
773        assert!(result.is_some());
774        let path = result.unwrap();
775        assert!(path.ends_with("src/helper.h"));
776    }
777
778    #[test]
779    fn test_resolve_c_include_subdirectory() {
780        let result = resolve_c_include_to_path("utils/helper.h", Some("/project/src/main.c"));
781
782        assert!(result.is_some());
783        let path = result.unwrap();
784        assert!(path.ends_with("src/utils/helper.h"));
785    }
786
787    #[test]
788    fn test_resolve_c_include_parent_directory() {
789        let result = resolve_c_include_to_path("../include/common.h", Some("/project/src/main.c"));
790
791        assert!(result.is_some());
792        let path = result.unwrap();
793        assert!(path.contains("include") && path.contains("common.h"));
794    }
795
796    #[test]
797    fn test_resolve_c_include_no_current_file() {
798        let result = resolve_c_include_to_path("helper.h", None);
799
800        assert!(result.is_none());
801    }
802}
803
804#[cfg(test)]
805mod dependency_extraction_tests {
806    use super::*;
807
808    #[test]
809    fn test_extract_basic_includes() {
810        let source = r#"
811            #include <stdio.h>
812            #include <stdlib.h>
813            #include "utils.h"
814            #include "math/vector.h"
815        "#;
816
817        let deps = CDependencyExtractor::extract_dependencies(source).unwrap();
818
819        assert_eq!(deps.len(), 4, "Should extract 4 include statements");
820        assert!(deps.iter().any(|d| d.imported_path == "stdio.h"));
821        assert!(deps.iter().any(|d| d.imported_path == "stdlib.h"));
822        assert!(deps.iter().any(|d| d.imported_path == "utils.h"));
823        assert!(deps.iter().any(|d| d.imported_path == "math/vector.h"));
824    }
825
826    #[test]
827    fn test_macro_includes_filtered() {
828        let source = r#"
829            #include <stdio.h>
830            #include "config.h"
831
832            // Macro-based includes - should be filtered out
833            #define HEADER_NAME "dynamic.h"
834            #include HEADER_NAME
835
836            #define STRINGIFY(x) #x
837            #include STRINGIFY(runtime_header.h)
838
839            // Conditional includes with macros
840            #ifdef USE_FEATURE_X
841            #define FEATURE_HEADER <feature_x.h>
842            #include FEATURE_HEADER
843            #endif
844        "#;
845
846        let deps = CDependencyExtractor::extract_dependencies(source).unwrap();
847
848        // Should only find static includes (stdio.h, config.h)
849        // Macro-based includes are filtered (not string_literal or system_lib_string nodes)
850        assert_eq!(deps.len(), 2, "Should extract 2 static includes only");
851
852        assert!(deps.iter().any(|d| d.imported_path == "stdio.h"));
853        assert!(deps.iter().any(|d| d.imported_path == "config.h"));
854
855        // Verify macro-based includes are NOT captured
856        assert!(!deps.iter().any(|d| d.imported_path.contains("HEADER_NAME")));
857        assert!(!deps.iter().any(|d| d.imported_path.contains("dynamic.h")));
858        assert!(
859            !deps
860                .iter()
861                .any(|d| d.imported_path.contains("runtime_header"))
862        );
863        assert!(
864            !deps
865                .iter()
866                .any(|d| d.imported_path.contains("FEATURE_HEADER"))
867        );
868    }
869
870    #[test]
871    fn test_include_classification() {
872        let source = r#"
873            #include <stdio.h>
874            #include "utils.h"
875            #include <mylib/api.h>
876        "#;
877
878        let deps = CDependencyExtractor::extract_dependencies(source).unwrap();
879
880        // Check stdlib classification
881        let stdio_dep = deps.iter().find(|d| d.imported_path == "stdio.h").unwrap();
882        assert!(
883            matches!(stdio_dep.import_type, ImportType::Stdlib),
884            "stdio.h should be classified as Stdlib"
885        );
886
887        // Check internal classification (quoted includes)
888        let utils_dep = deps.iter().find(|d| d.imported_path == "utils.h").unwrap();
889        assert!(
890            matches!(utils_dep.import_type, ImportType::Internal),
891            "quoted include should be classified as Internal"
892        );
893
894        // Check external classification (non-stdlib angle bracket includes)
895        let mylib_dep = deps
896            .iter()
897            .find(|d| d.imported_path == "mylib/api.h")
898            .unwrap();
899        assert!(
900            matches!(mylib_dep.import_type, ImportType::External),
901            "non-stdlib angle bracket include should be classified as External"
902        );
903    }
904}