1use std::fs;
6use std::path::Path;
7
8use anyhow::{Context, Result};
9use syn::spanned::Spanned;
10use syn::{File, Item, ItemEnum, ItemFn, ItemMod, ItemStruct};
11use walkdir::{DirEntry, WalkDir};
12
13use crate::complexity::cognitive_complexity;
14use crate::impl_collector::{end_line, is_test_attrs, qualified_name, start_line, visit_impl};
15use crate::model::{PackageContext, SourceFunction};
16
17pub(crate) struct FileWalker<'a> {
18 package: &'a PackageContext,
19 include_test_targets: bool,
20 exclude_paths: &'a [String],
21}
22
23impl<'a> FileWalker<'a> {
24 pub(crate) fn new(package: &'a PackageContext) -> Self {
25 Self {
26 package,
27 include_test_targets: package.include_test_targets,
28 exclude_paths: &package.exclude_paths,
29 }
30 }
31
32 pub(crate) fn process_source_root(&self, source_root: &Path) -> Result<Vec<SourceFunction>> {
33 if !source_root.exists() {
34 return Ok(Vec::new());
35 }
36
37 let mut functions = Vec::new();
38 for entry in WalkDir::new(source_root)
39 .into_iter()
40 .filter_map(|entry| entry.ok())
41 .filter(|entry| entry.file_type().is_file())
42 .filter(|entry| {
43 entry
44 .path()
45 .extension()
46 .is_some_and(|extension| extension == "rs")
47 })
48 {
49 functions.extend(self.process_entry(source_root, &entry)?);
50 }
51 Ok(functions)
52 }
53
54 fn process_entry(&self, source_root: &Path, entry: &DirEntry) -> Result<Vec<SourceFunction>> {
55 let file_path = entry.path();
56 let relative_file = relative_file(&self.package.manifest_dir, file_path);
57 if !is_selected_relative_file(&relative_file, self.include_test_targets)
58 || !is_selected_source_file(
59 &self.package.manifest_dir,
60 file_path,
61 self.include_test_targets,
62 )
63 || is_excluded_relative_file(&relative_file, self.exclude_paths)
64 {
65 return Ok(Vec::new());
66 }
67 let module_prefix = module_prefix(source_root, file_path);
68 let source = fs::read_to_string(file_path)
69 .with_context(|| format!("failed to read source file {}", file_path.display()))?;
70 let syntax = syn::parse_file(&source)
71 .with_context(|| format!("failed to parse source file {}", file_path.display()))?;
72
73 let mut functions = Vec::new();
74 visit_items(
75 self.package,
76 &syntax,
77 &normalize_path(file_path),
78 &relative_file,
79 &module_prefix,
80 &mut Vec::new(),
81 &mut functions,
82 );
83 Ok(functions)
84 }
85}
86
87pub fn normalize_path(path: &Path) -> String {
88 let normalized = path
89 .canonicalize()
90 .unwrap_or_else(|_| path.to_path_buf())
91 .to_string_lossy()
92 .replace('\\', "/");
93 if cfg!(windows) {
94 normalized.to_lowercase()
95 } else {
96 normalized
97 }
98}
99
100pub fn relative_file(base_dir: &Path, file_path: &Path) -> String {
101 file_path
102 .strip_prefix(base_dir)
103 .unwrap_or(file_path)
104 .to_string_lossy()
105 .replace('\\', "/")
106}
107
108pub(crate) fn is_selected_source_file(
109 base_dir: &Path,
110 file_path: &Path,
111 include_test_targets: bool,
112) -> bool {
113 let base_dir = normalize_path(base_dir);
114 let file_path = normalize_path(file_path);
115 let Some(relative) = file_path.strip_prefix(&base_dir) else {
116 return true;
117 };
118 let relative = relative.strip_prefix('/').unwrap_or(relative);
119
120 let mut components = relative.split('/');
121 let Some(first) = components.next() else {
122 return true;
123 };
124
125 if matches!(first, "examples" | "benches") {
126 return false;
127 }
128
129 if first == "tests" {
130 return include_test_targets;
131 }
132
133 !relative.ends_with("/build.rs") && relative != "build.rs"
134}
135
136pub fn is_excluded_relative_file(relative_file: &str, exclude_paths: &[String]) -> bool {
137 exclude_paths.iter().any(|prefix| {
138 let normalised = prefix.replace('\\', "/");
139 let prefix_with_slash = if normalised.ends_with('/') {
140 normalised.clone()
141 } else {
142 format!("{}/", normalised)
143 };
144 relative_file.starts_with(&prefix_with_slash) || relative_file == normalised
145 })
146}
147
148pub fn is_selected_relative_file(relative_file: &str, include_test_targets: bool) -> bool {
149 !relative_file.starts_with("examples/")
150 && !relative_file.starts_with("benches/")
151 && relative_file != "build.rs"
152 && (include_test_targets || !relative_file.starts_with("tests/"))
153}
154
155pub(crate) fn module_prefix(source_root: &Path, file_path: &Path) -> Vec<String> {
156 let relative = file_path.strip_prefix(source_root).unwrap_or(file_path);
157 let mut prefix = relative
158 .parent()
159 .map(|parent| {
160 parent
161 .components()
162 .map(|component| component.as_os_str().to_string_lossy().to_string())
163 .collect::<Vec<_>>()
164 })
165 .unwrap_or_default();
166 let file_stem = file_path
167 .file_stem()
168 .and_then(|stem| stem.to_str())
169 .unwrap_or_default();
170 if !matches!(file_stem, "lib" | "main" | "mod") {
171 prefix.push(file_stem.to_string());
172 }
173 prefix
174}
175
176pub(crate) fn visit_items(
177 package: &PackageContext,
178 syntax: &File,
179 path_key: &str,
180 relative_file: &str,
181 module_prefix: &[String],
182 inline_modules: &mut Vec<String>,
183 functions: &mut Vec<SourceFunction>,
184) {
185 for item in &syntax.items {
186 visit_item(
187 package,
188 item,
189 path_key,
190 relative_file,
191 module_prefix,
192 inline_modules,
193 functions,
194 );
195 }
196}
197
198pub(crate) fn visit_item(
199 package: &PackageContext,
200 item: &Item,
201 path_key: &str,
202 relative_file: &str,
203 module_prefix: &[String],
204 inline_modules: &mut Vec<String>,
205 functions: &mut Vec<SourceFunction>,
206) {
207 match item {
208 Item::Fn(item_fn) => {
209 if let Some(function) = record_function(
210 package,
211 item_fn,
212 None,
213 path_key,
214 relative_file,
215 module_prefix,
216 inline_modules,
217 ) {
218 functions.push(function);
219 }
220 }
221 Item::Impl(item_impl) if !is_test_attrs(&item_impl.attrs) => visit_impl(
222 package,
223 item_impl,
224 path_key,
225 relative_file,
226 module_prefix,
227 inline_modules,
228 functions,
229 ),
230 Item::Mod(item_mod) if !is_test_attrs(&item_mod.attrs) => visit_module(
231 package,
232 item_mod,
233 path_key,
234 relative_file,
235 module_prefix,
236 inline_modules,
237 functions,
238 ),
239 Item::Enum(ItemEnum { .. }) | Item::Struct(ItemStruct { .. }) => {}
240 _ => {}
241 }
242}
243
244pub(crate) fn visit_module(
245 package: &PackageContext,
246 item_mod: &ItemMod,
247 path_key: &str,
248 relative_file: &str,
249 module_prefix: &[String],
250 inline_modules: &mut Vec<String>,
251 functions: &mut Vec<SourceFunction>,
252) {
253 let Some((_, items)) = &item_mod.content else {
254 return;
255 };
256
257 inline_modules.push(item_mod.ident.to_string());
258 for item in items {
259 visit_item(
260 package,
261 item,
262 path_key,
263 relative_file,
264 module_prefix,
265 inline_modules,
266 functions,
267 );
268 }
269 inline_modules.pop();
270}
271
272pub(crate) fn record_function(
273 package: &PackageContext,
274 item_fn: &ItemFn,
275 receiver: Option<&str>,
276 path_key: &str,
277 relative_file: &str,
278 module_prefix: &[String],
279 inline_modules: &[String],
280) -> Option<SourceFunction> {
281 if is_test_attrs(&item_fn.attrs) {
282 return None;
283 }
284
285 let name = qualified_name(
286 module_prefix,
287 inline_modules,
288 receiver,
289 &item_fn.sig.ident.to_string(),
290 );
291 Some(SourceFunction {
292 package_name: package.name.clone(),
293 name,
294 path_key: path_key.to_string(),
295 relative_file: relative_file.to_string(),
296 line: start_line(item_fn.sig.ident.span()),
297 end_line: end_line(item_fn.span()),
298 complexity: cognitive_complexity(&item_fn.block),
299 })
300}