feat: implement all P1/P2/P3/P4 improvements from issue backlog
P1 correctness: - filter test files by default (--include-tests to opt in) - per-module diagrams show cross-module dependency arrows - qualified type names (Module::TypeName) fix false edges from duplicate names P2 output richness: - method parameter types and return types in class diagrams (Rust + Python) - Python pyproject.toml project analyzer (--level project for monorepos) P3 unique value: - boundary rules in archlens.toml ([rules] allow/deny, --strict enforcement) P4 nice to have: - dependency weight labels on module arrows (--no-weights to disable) - --watch mode with 500ms debounce - D2 renderer adapter (--format d2) - interactive self-contained HTML viewer (--format html) - git-aware incremental analysis (--since <ref>)
This commit is contained in:
@@ -42,6 +42,27 @@ impl WalkdirDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_test_file(path: &Path, language: Language) -> bool {
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default();
|
||||
let in_tests_dir = path
|
||||
.parent()
|
||||
.map(|p| p.components().any(|c| c.as_os_str() == "tests"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_tests_dir {
|
||||
return true;
|
||||
}
|
||||
|
||||
match language {
|
||||
Language::Rust => stem.ends_with("_test") || stem.ends_with("_tests"),
|
||||
Language::Python => stem.starts_with("test_") || stem.ends_with("_test"),
|
||||
Language::CSharp => stem.ends_with("Tests") || stem.ends_with("Test"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_excluded(path: &Path, root: &Path, excludes: &[String]) -> bool {
|
||||
let relative = path.strip_prefix(root).unwrap_or(path);
|
||||
let relative_str = relative.to_string_lossy();
|
||||
@@ -88,6 +109,18 @@ impl FileDiscovery for WalkdirDiscovery {
|
||||
}
|
||||
|
||||
if let Some(language) = Self::detect_language(path) {
|
||||
if !config.include_tests() && Self::is_test_file(path, language) {
|
||||
continue;
|
||||
}
|
||||
if let Some(changed) = config.changed_files() {
|
||||
let relative = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
|
||||
if !changed
|
||||
.iter()
|
||||
.any(|c| relative.ends_with(c.as_str()) || c.ends_with(relative.as_ref()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let absolute = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let file_path = FilePath::new(&absolute.to_string_lossy())
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))?;
|
||||
|
||||
@@ -58,6 +58,100 @@ fn respects_exclude_patterns() {
|
||||
assert!(!files.iter().any(|f| f.path().as_str().contains("billing")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_python_test_prefix_files_by_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("orders.py"), "class Order: pass").unwrap();
|
||||
fs::write(dir.path().join("test_orders.py"), "class TestOrder: pass").unwrap();
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].path().as_str().ends_with("orders.py"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_python_test_suffix_files_by_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("orders.py"), "class Order: pass").unwrap();
|
||||
fs::write(dir.path().join("orders_test.py"), "class OrderTest: pass").unwrap();
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].path().as_str().ends_with("orders.py"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_files_in_tests_directory_by_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("tests")).unwrap();
|
||||
fs::write(dir.path().join("orders.py"), "class Order: pass").unwrap();
|
||||
fs::write(dir.path().join("tests/helpers.py"), "class Helper: pass").unwrap();
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].path().as_str().ends_with("orders.py"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_rust_test_files_by_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("orders.rs"), "struct Order;").unwrap();
|
||||
fs::write(dir.path().join("orders_tests.rs"), "struct OrdersTests;").unwrap();
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].path().as_str().ends_with("orders.rs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excludes_rust_files_in_tests_directory_by_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("tests")).unwrap();
|
||||
fs::write(dir.path().join("lib.rs"), "struct Lib;").unwrap();
|
||||
fs::write(
|
||||
dir.path().join("tests/integration.rs"),
|
||||
"struct IntegrationTest;",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].path().as_str().ends_with("lib.rs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn include_tests_flag_re_enables_test_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("orders.py"), "class Order: pass").unwrap();
|
||||
fs::write(dir.path().join("test_orders.py"), "class TestOrder: pass").unwrap();
|
||||
|
||||
let config = AnalysisConfig::default().with_include_tests(true);
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery.discover(dir.path(), &config).unwrap();
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_directory_returns_no_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user