···11-pub fn add(left: u64, right: u64) -> u64 {
22- left + right
33-}
11+pub mod cloner;
22+pub mod complexity;
33+pub mod config;
44+pub mod error;
55+pub mod loader;
66+pub mod reporter;
77+pub mod tokenizer;
4855-#[cfg(test)]
66-mod tests {
77- use super::*;
88-99- #[test]
1010- fn it_works() {
1111- let result = add(2, 2);
1212- assert_eq!(result, 4);
1313- }
1414-}
99+pub use error::{MccabreError, Result};
+203
crates/core/src/loader.rs
···11+use crate::error::{MccabreError, Result};
22+use crate::tokenizer::Language;
33+use ignore::WalkBuilder;
44+use std::path::{Path, PathBuf};
55+use std::{fs, io};
66+77+/// File entry with source code and metadata
88+#[derive(Debug, Clone)]
99+pub struct SourceFile {
1010+ pub path: PathBuf,
1111+ pub content: String,
1212+ pub language: Language,
1313+}
1414+1515+/// File loader that respects .gitignore and supports various input types
1616+pub struct FileLoader {
1717+ /// Whether to respect .gitignore files
1818+ respect_gitignore: bool,
1919+}
2020+2121+impl Default for FileLoader {
2222+ fn default() -> Self {
2323+ Self { respect_gitignore: true }
2424+ }
2525+}
2626+2727+impl FileLoader {
2828+ pub fn new() -> Self {
2929+ Self::default()
3030+ }
3131+3232+ /// Enable or disable gitignore awareness
3333+ pub fn with_gitignore(mut self, respect: bool) -> Self {
3434+ self.respect_gitignore = respect;
3535+ self
3636+ }
3737+3838+ /// Load files from a path (file, directory, or list)
3939+ pub fn load<P: AsRef<Path>>(&self, path: P) -> Result<Vec<SourceFile>> {
4040+ let path = path.as_ref();
4141+4242+ if path.is_file() {
4343+ let file = self.load_file(path)?;
4444+ Ok(vec![file])
4545+ } else if path.is_dir() {
4646+ self.load_directory(path)
4747+ } else {
4848+ Err(MccabreError::FileRead {
4949+ path: path.to_path_buf(),
5050+ source: std::io::Error::new(std::io::ErrorKind::NotFound, "Path is neither a file nor a directory"),
5151+ })
5252+ }
5353+ }
5454+5555+ /// Load multiple paths
5656+ pub fn load_multiple<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<SourceFile>> {
5757+ let mut files = Vec::new();
5858+5959+ for path in paths {
6060+ let mut loaded = self.load(path)?;
6161+ files.append(&mut loaded);
6262+ }
6363+6464+ files.sort_by(|a, b| a.path.cmp(&b.path));
6565+ files.dedup_by(|a, b| a.path == b.path);
6666+6767+ Ok(files)
6868+ }
6969+7070+ /// Load a single file
7171+ fn load_file(&self, path: &Path) -> Result<SourceFile> {
7272+ let language = Language::from_path(path)?;
7373+ let content =
7474+ fs::read_to_string(path).map_err(|e| MccabreError::FileRead { path: path.to_path_buf(), source: e })?;
7575+7676+ Ok(SourceFile { path: path.to_path_buf(), content, language })
7777+ }
7878+7979+ /// Load all supported files from a directory
8080+ fn load_directory(&self, dir: &Path) -> Result<Vec<SourceFile>> {
8181+ let mut files = Vec::new();
8282+8383+ let walker = WalkBuilder::new(dir)
8484+ .standard_filters(self.respect_gitignore)
8585+ .hidden(false)
8686+ .parents(true)
8787+ .build();
8888+8989+ for entry in walker {
9090+ let entry = entry.map_err(|e| MccabreError::Io(io::Error::other(e.to_string())))?;
9191+ let path = entry.path();
9292+9393+ if !path.is_file() {
9494+ continue;
9595+ }
9696+9797+ match self.load_file(path) {
9898+ Ok(file) => files.push(file),
9999+ Err(MccabreError::UnsupportedFileType(_)) => continue,
100100+ Err(e) => return Err(e),
101101+ }
102102+ }
103103+104104+ Ok(files)
105105+ }
106106+}
107107+108108+#[cfg(test)]
109109+mod tests {
110110+ use super::*;
111111+ use std::fs;
112112+ use tempfile::TempDir;
113113+114114+ #[test]
115115+ fn test_load_single_file() -> Result<()> {
116116+ let temp_dir = TempDir::new().unwrap();
117117+ let file_path = temp_dir.path().join("test.rs");
118118+ fs::write(&file_path, "fn main() {}").unwrap();
119119+120120+ let loader = FileLoader::new();
121121+ let files = loader.load(&file_path)?;
122122+123123+ assert_eq!(files.len(), 1);
124124+ assert_eq!(files[0].content, "fn main() {}");
125125+ assert_eq!(files[0].language, Language::Rust);
126126+127127+ Ok(())
128128+ }
129129+130130+ #[test]
131131+ fn test_load_directory() -> Result<()> {
132132+ let temp_dir = TempDir::new().unwrap();
133133+ fs::write(temp_dir.path().join("file1.rs"), "fn test1() {}").unwrap();
134134+ fs::write(temp_dir.path().join("file2.js"), "function test2() {}").unwrap();
135135+ fs::write(temp_dir.path().join("readme.txt"), "Not code").unwrap();
136136+137137+ let loader = FileLoader::new();
138138+ let files = loader.load(temp_dir.path())?;
139139+140140+ assert_eq!(files.len(), 2);
141141+142142+ let has_rust = files.iter().any(|f| f.path.ends_with("file1.rs"));
143143+ let has_js = files.iter().any(|f| f.path.ends_with("file2.js"));
144144+ assert!(has_rust);
145145+ assert!(has_js);
146146+147147+ Ok(())
148148+ }
149149+150150+ #[test]
151151+ fn test_gitignore_respected() -> Result<()> {
152152+ let temp_dir = TempDir::new().unwrap();
153153+ fs::write(temp_dir.path().join("included.rs"), "fn included() {}").unwrap();
154154+155155+ let ignored_dir = temp_dir.path().join("build");
156156+ fs::create_dir(&ignored_dir).unwrap();
157157+ fs::write(ignored_dir.join("excluded.rs"), "fn excluded() {}").unwrap();
158158+159159+ fs::write(temp_dir.path().join(".gitignore"), "build/\n").unwrap();
160160+161161+ let loader_with_gitignore = FileLoader::new().with_gitignore(true);
162162+ let files_with = loader_with_gitignore.load(temp_dir.path())?;
163163+164164+ let loader_without_gitignore = FileLoader::new().with_gitignore(false);
165165+ let files_without = loader_without_gitignore.load(temp_dir.path())?;
166166+167167+ assert!(files_with.iter().any(|f| f.path.ends_with("included.rs")));
168168+169169+ assert!(files_without.iter().any(|f| f.path.ends_with("included.rs")));
170170+ assert!(files_without.iter().any(|f| f.path.ends_with("excluded.rs")));
171171+172172+ Ok(())
173173+ }
174174+175175+ #[test]
176176+ fn test_unsupported_file_type() {
177177+ let temp_dir = TempDir::new().unwrap();
178178+ let file_path = temp_dir.path().join("test.xyz");
179179+ fs::write(&file_path, "random content").unwrap();
180180+181181+ let loader = FileLoader::new();
182182+ let result = loader.load(&file_path);
183183+184184+ assert!(matches!(result, Err(MccabreError::UnsupportedFileType(_))));
185185+ }
186186+187187+ #[test]
188188+ fn test_load_multiple() -> Result<()> {
189189+ let temp_dir = TempDir::new().unwrap();
190190+ let file1 = temp_dir.path().join("test1.rs");
191191+ let file2 = temp_dir.path().join("test2.js");
192192+193193+ fs::write(&file1, "fn test1() {}").unwrap();
194194+ fs::write(&file2, "function test2() {}").unwrap();
195195+196196+ let loader = FileLoader::new();
197197+ let files = loader.load_multiple(&[&file1, &file2])?;
198198+199199+ assert_eq!(files.len(), 2);
200200+201201+ Ok(())
202202+ }
203203+}