From 3c62101ecfbcf4d433433d4ea924a2565c3dbd3d Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 5 Feb 2026 14:40:55 +0000 Subject: [PATCH] Initial upload with CI/CD workflow --- src/core/mod.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/core/mod.rs diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..edd578d --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,151 @@ +use anyhow::{Context, Result}; +use ignore::WalkBuilder; +use std::path::PathBuf; + +use crate::models::{ + AnalysisSummary, ByLanguage, ByPriority, CommentType, Config, FileLocation, Priority, + TechDebtItem, +}; + +mod language; +pub use language::{Language, LanguageParser}; + +pub struct Analyzer { + path: PathBuf, + config: Config, +} + +impl Analyzer { + pub fn new(path: &PathBuf, config_path: &Option) -> Result { + let config = load_config(config_path)?; + Ok(Self { + path: path.clone(), + config, + }) + } + + pub fn analyze(&self) -> Result> { + let mut items = Vec::new(); + + let walker = WalkBuilder::new(&self.path) + .hidden(true) + .git_global(true) + .git_ignore(true) + .require_git(false) + .build(); + + for result in walker { + match result { + Ok(entry) => { + let path = entry.path(); + + if !self.should_include(path) { + continue; + } + + if let Some(language) = Language::from_path(path) { + match self.parse_file(path, &language) { + Ok(mut file_items) => items.append(&mut file_items), + Err(e) => { + eprintln!("Warning: Failed to parse {}: {}", path.display(), e) + } + } + } + } + Err(e) => eprintln!("Warning: Error walking directory: {}", e), + } + } + + items.sort_by(|a, b| { + b.priority + .cmp(&a.priority) + .then_with(|| a.location.line.cmp(&b.location.line)) + }); + + Ok(items) + } + + fn should_include(&self, path: &PathBuf) -> bool { + if !path.is_file() { + return false; + } + + if let Some(ext) = path.extension() { + if let Some(ext_str) = ext.to_str() { + let ext_with_dot = format!(".{}", ext_str); + if !self.config.extensions.contains(&ext_with_dot) { + return false; + } + } + } + + for pattern in &self.config.ignore { + if match_ignore_pattern(path, pattern) { + return false; + } + } + + true + } + + fn parse_file( + &self, + path: &PathBuf, + language: &Language, + ) -> Result> { + let content = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {}", path.display()))?; + + let parser = LanguageParser::new(language.clone()); + parser.parse(&content, path, &self.config.patterns) + } +} + +fn match_ignore_pattern(path: &PathBuf, pattern: &str) -> bool { + if pattern.ends_with("/**") { + let prefix = &pattern[..pattern.len() - 3]; + if let Some(path_str) = path.to_str() { + return path_str.starts_with(prefix) + || path_str.contains(&format!("{}/", prefix)); + } + } else if let Some(file_name) = path.file_name() { + if let Some(file_name_str) = file_name.to_str() { + return glob::Pattern::new(pattern) + .ok() + .map(|p| p.matches(file_name_str)) + .unwrap_or(false); + } + } + false +} + +fn load_config(config_path: &Option) -> Result { + let config_path = if let Some(path) = config_path { + path.clone() + } else { + std::env::current_dir()? + .join("techdebt.yaml") + }; + + if config_path.exists() { + let content = std::fs::read_to_string(&config_path)?; + let config: Config = serde_yaml::from_str(&content)?; + return Ok(config); + } + + Ok(Config::default()) +} + +pub fn summarize(items: &[TechDebtItem]) -> AnalysisSummary { + let by_priority = ByPriority::from_items(items); + let by_language = ByLanguage::from_items(items); + let complexity_distribution = + crate::models::ComplexityDistribution::from_items(items); + + AnalysisSummary { + total_items: items.len(), + by_priority, + by_language, + complexity_distribution, + } +}