mati_core/analysis/resolvers/
cpp.rs1use 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
52pub 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 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 if file_index.contains(include_path) {
88 return Some(include_path.to_string());
89 }
90
91 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 Path::new(include_path).extension().is_none() {
101 for ext in &[".hpp", ".hxx", ".hh", ".h"] {
102 let with_ext = format!("{include_path}{ext}");
103 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 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}