Skip to main content

mati_core/analysis/resolvers/
c.rs

1//! C import resolver.
2//!
3//! Resolves quoted includes (`#include "myheader.h"`) relative to the
4//! importing file's directory, then the project root. Angle-bracket
5//! includes (`#include <stdio.h>`) are classified as `External` at parse
6//! time and never reach this resolver.
7//!
8//! # Known limitations
9//!
10//! - No `-I` include path support — only checks relative to the importing
11//!   file, project root, and `include/` / `src/` directories
12//! - Conditional includes (`#ifdef`-guarded `#include`) are always counted
13//!   regardless of which branch is active at compile time
14//! - `#include` directives using macros (`#include HEADER_NAME`) are not
15//!   resolved — the macro is opaque to tree-sitter
16//! - Symlinked header directories are not followed beyond the walker's
17//!   default traversal
18//! - Multi-level relative paths (`../../common/types.h`) resolve correctly
19//!   but only if the target is within the walked repo root
20//!
21//! These limitations mean edge counts for C projects with complex build
22//! systems (CMake, autotools) will be lower than the actual dependency
23//! graph. Projects using a flat `include/` layout will get better coverage.
24
25use std::path::Path;
26
27use super::{FileIndex, LanguageResolver};
28use crate::analysis::parser::ImportStatement;
29use crate::analysis::walker::Language;
30
31pub struct CResolver;
32
33impl LanguageResolver for CResolver {
34    fn resolve(
35        &self,
36        import: &ImportStatement,
37        importing_file: &str,
38        file_index: &FileIndex,
39    ) -> Option<String> {
40        resolve_c_include(&import.path, importing_file, file_index)
41    }
42
43    fn language(&self) -> Language {
44        Language::C
45    }
46
47    fn name(&self) -> &'static str {
48        "c"
49    }
50}
51
52/// Resolve an angle-bracket include path against the file index.
53///
54/// Called from `build_edges` for C files whose `#include <path>` was
55/// classified as `External` at parse time.  If the path resolves to a
56/// known repo file it is actually internal.
57pub fn resolve_angle_bracket(
58    include_path: &str,
59    importing_file: &str,
60    file_index: &FileIndex,
61) -> Option<String> {
62    resolve_c_include(include_path, importing_file, file_index)
63}
64
65fn resolve_c_include(
66    include_path: &str,
67    importing_file: &str,
68    file_index: &FileIndex,
69) -> Option<String> {
70    let parent = Path::new(importing_file)
71        .parent()
72        .map(|p| p.to_string_lossy().into_owned())
73        .unwrap_or_default();
74
75    // Try relative to importing file's directory
76    let relative = if parent.is_empty() {
77        include_path.to_string()
78    } else {
79        format!("{parent}/{include_path}")
80    };
81    if file_index.contains(&relative) {
82        return Some(relative);
83    }
84
85    // Try from project root
86    if file_index.contains(include_path) {
87        return Some(include_path.to_string());
88    }
89
90    // Try under common include directories
91    for prefix in &["include", "src"] {
92        let candidate = format!("{prefix}/{include_path}");
93        if file_index.contains(&candidate) {
94            return Some(candidate);
95        }
96    }
97
98    None
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::analysis::parser::import::ImportKind;
105
106    fn idx(paths: &[&str]) -> FileIndex {
107        FileIndex::new(paths.iter().map(|s| s.to_string()))
108    }
109
110    fn import(path: &str) -> ImportStatement {
111        ImportStatement::new(path, ImportKind::Relative, 1)
112    }
113
114    #[test]
115    fn relative_include_resolves() {
116        let file_index = idx(&["src/main.c", "src/utils.h"]);
117        let result = CResolver.resolve(&import("utils.h"), "src/main.c", &file_index);
118        assert_eq!(result, Some("src/utils.h".into()));
119    }
120
121    #[test]
122    fn root_include_resolves() {
123        let file_index = idx(&["src/main.c", "config.h"]);
124        let result = CResolver.resolve(&import("config.h"), "src/main.c", &file_index);
125        assert_eq!(result, Some("config.h".into()));
126    }
127
128    #[test]
129    fn include_dir_resolves() {
130        let file_index = idx(&["src/main.c", "include/types.h"]);
131        let result = CResolver.resolve(&import("types.h"), "src/main.c", &file_index);
132        assert_eq!(result, Some("include/types.h".into()));
133    }
134
135    #[test]
136    fn nonexistent_returns_none() {
137        let file_index = idx(&["src/main.c"]);
138        assert_eq!(
139            CResolver.resolve(&import("missing.h"), "src/main.c", &file_index),
140            None
141        );
142    }
143}