Skip to main content

mati_core/analysis/resolvers/
cpp.rs

1//! C++ import resolver.
2//!
3//! Same strategy as the C resolver but also tries C++ header extensions
4//! (`.hpp`, `.hxx`, `.hh`). Angle-bracket includes are classified as
5//! `External` at parse time.
6//!
7//! # Known limitations
8//!
9//! - No `-I` include path support — only checks relative to the importing
10//!   file, project root, and `include/` / `src/` directories
11//! - Template-heavy headers (Boost, Eigen, STL implementations) produce
12//!   no edges since they use angle-bracket includes
13//! - Module imports (`import <module>;` in C++20) are not recognized —
14//!   only preprocessor `#include` directives are parsed
15//! - Extensionless includes (`#include "mylib"`) try `.hpp`, `.hxx`,
16//!   `.hh`, `.h` in order — but not `.H` or other uncommon extensions
17//! - Conditional includes and macro-based includes are not resolved
18//!   (same as the C resolver)
19//! - PCH (precompiled header) references are not detected
20//!
21//! These limitations mean C++ projects relying heavily on template
22//! libraries or C++20 modules will have lower edge counts. Projects
23//! using quoted includes with standard extensions get good coverage.
24
25use std::path::Path;
26
27use super::{FileIndex, LanguageResolver};
28use crate::analysis::parser::ImportStatement;
29use crate::analysis::walker::Language;
30
31pub struct CppResolver;
32
33impl LanguageResolver for CppResolver {
34    fn resolve(
35        &self,
36        import: &ImportStatement,
37        importing_file: &str,
38        file_index: &FileIndex,
39    ) -> Option<String> {
40        resolve_cpp_include(&import.path, importing_file, file_index)
41    }
42
43    fn language(&self) -> Language {
44        Language::Cpp
45    }
46
47    fn name(&self) -> &'static str {
48        "cpp"
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 — the edge builder will
57/// create an `Imports` edge instead of skipping.
58pub fn resolve_angle_bracket(
59    include_path: &str,
60    importing_file: &str,
61    file_index: &FileIndex,
62) -> Option<String> {
63    resolve_cpp_include(include_path, importing_file, file_index)
64}
65
66fn resolve_cpp_include(
67    include_path: &str,
68    importing_file: &str,
69    file_index: &FileIndex,
70) -> Option<String> {
71    let parent = Path::new(importing_file)
72        .parent()
73        .map(|p| p.to_string_lossy().into_owned())
74        .unwrap_or_default();
75
76    // Try relative to importing file's directory
77    let relative = if parent.is_empty() {
78        include_path.to_string()
79    } else {
80        format!("{parent}/{include_path}")
81    };
82    if file_index.contains(&relative) {
83        return Some(relative);
84    }
85
86    // Try from project root
87    if file_index.contains(include_path) {
88        return Some(include_path.to_string());
89    }
90
91    // Try under common include directories
92    for prefix in &["include", "src"] {
93        let candidate = format!("{prefix}/{include_path}");
94        if file_index.contains(&candidate) {
95            return Some(candidate);
96        }
97    }
98
99    // If the include has no extension, try C++ header extensions
100    if Path::new(include_path).extension().is_none() {
101        for ext in &[".hpp", ".hxx", ".hh", ".h"] {
102            let with_ext = format!("{include_path}{ext}");
103            // Try relative
104            let rel = if parent.is_empty() {
105                with_ext.clone()
106            } else {
107                format!("{parent}/{with_ext}")
108            };
109            if file_index.contains(&rel) {
110                return Some(rel);
111            }
112            // Try root
113            if file_index.contains(&with_ext) {
114                return Some(with_ext);
115            }
116        }
117    }
118
119    None
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::analysis::parser::import::ImportKind;
126
127    fn idx(paths: &[&str]) -> FileIndex {
128        FileIndex::new(paths.iter().map(|s| s.to_string()))
129    }
130
131    fn import(path: &str) -> ImportStatement {
132        ImportStatement::new(path, ImportKind::Relative, 1)
133    }
134
135    #[test]
136    fn relative_include_resolves() {
137        let file_index = idx(&["src/main.cpp", "src/utils.hpp"]);
138        let result = CppResolver.resolve(&import("utils.hpp"), "src/main.cpp", &file_index);
139        assert_eq!(result, Some("src/utils.hpp".into()));
140    }
141
142    #[test]
143    fn extensionless_include_tries_hpp() {
144        let file_index = idx(&["src/main.cpp", "src/utils.hpp"]);
145        let result = CppResolver.resolve(&import("utils"), "src/main.cpp", &file_index);
146        assert_eq!(result, Some("src/utils.hpp".into()));
147    }
148
149    #[test]
150    fn include_dir_resolves() {
151        let file_index = idx(&["src/main.cpp", "include/types.h"]);
152        let result = CppResolver.resolve(&import("types.h"), "src/main.cpp", &file_index);
153        assert_eq!(result, Some("include/types.h".into()));
154    }
155
156    #[test]
157    fn nonexistent_returns_none() {
158        let file_index = idx(&["src/main.cpp"]);
159        assert_eq!(
160            CppResolver.resolve(&import("missing.h"), "src/main.cpp", &file_index),
161            None
162        );
163    }
164}