重构: scan.rs拆4模块(stack/discover/readme/sample)共享常量提mod.rs

This commit is contained in:
lxy
2026-06-22 01:45:43 +08:00
parent 2a13f45650
commit d1d3871a88
6 changed files with 855 additions and 741 deletions
+114
View File
@@ -0,0 +1,114 @@
//! 项目技术栈探测 — 浅读根目录标志文件识别技术栈
//!
//! 纯函数、零状态、零外部依赖(仅 std + serde_json + anyhow)。
//! 只读根目录标志文件,不递归遍历(控制性能与安全)。
//!
//! 供 commands 层薄封装暴露为 IPC 命令,新建/导入项目时自动填充 `ProjectRecord.stack`;
//! 未来 df-ai/df-workflow 亦可复用以感知「项目是什么技术栈」。
//!
//! ## 模块拆分(2026-06-22,REFACTOR-260619-10)
//!
//! 原 `scan.rs`(741 行)按职责拆为 4 子模块,纯文件重组、零行为变化:
//! - [`stack`]:技术栈探测(`detect_stack` + package.json 解析)
//! - [`discover`]:历史项目发现/monorepo 展开(`discover_projects`/`is_monorepo`)
//! - [`readme`]:README 读取与首段抽取(`read_readme_raw`/`extract_description`)
//! - [`sample`]:项目采样(`collect_sample` + 目录树/清单/噪音目录)
//!
//! 跨模块共享的常量/helper 留在本 `mod.rs`(`SAMPLE_IGNORED_DIRS`/`truncate_chars`),
//! 子模块经 `use super::*` 引用。`pub use` 重导出保持外部 `df_project::scan::*` 路径不变。
//!
//! README 噪音处理与图片收集已抽离至 `scan_helpers`(2026-06-19 自底向上拆分),
//! 此处通过 `pub use` 重导出 `ImageRef` 等保持外部路径 `df_project::scan::*` 不变。
use std::path::Path;
#[path = "../scan_helpers.rs"]
mod scan_helpers;
pub use scan_helpers::ImageRef;
pub mod discover;
pub mod readme;
pub mod sample;
pub mod stack;
// ── 对外稳定 API 重导出(外部 `df_project::scan::*` 路径不变)──
pub use discover::{discover_projects, is_monorepo, DiscoveredProject};
pub use readme::extract_description;
pub use sample::{collect_sample, ProjectSample};
pub use stack::detect_stack;
// ============================================================
// 跨模块共享常量/helper
// ============================================================
/// 目录树/项目发现过滤的噪音目录(依赖产物/构建/缓存/IDE)。
///
/// 同时被 [`discover`] 与 [`sample`] 引用,故置于本 `mod.rs`,`pub(super)` 暴露给子模块。
pub(super) const SAMPLE_IGNORED_DIRS: &[&str] = &[
"node_modules", "target", ".git", "dist", "build", ".next", "venv", ".venv",
"__pycache__", ".idea", ".vscode", ".cache", "out", "coverage", ".svelte-kit",
".turbo", ".angular", ".gradle", "vendor",
];
/// 按字符数截断(避免截断 UTF-8 多字节边界)。
///
/// 被 [`readme`](crate::scan::readme) 与 [`sample`](crate::scan::sample) 引用,故置于本 `mod.rs`。
pub(super) fn truncate_chars(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let truncated: String = s.chars().take(max).collect();
format!("{}…(已截断)", truncated)
}
/// 规范化路径用于比较:canonicalize 解析绝对规范路径(失败降级),
/// 统一正斜杠 + 小写。防 `C:\a\b` vs `C:/a/b/` 绕过重复检查。
/// 注:仅用于比较,存库保留用户输入的原始可读路径。
pub fn normalize_path(p: &str) -> String {
match Path::new(p).canonicalize() {
Ok(abs) => abs.to_string_lossy().replace('\\', "/").to_lowercase(),
Err(_) => p
.trim_end_matches(['\\', '/'])
.replace('\\', "/")
.to_lowercase(),
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── normalize_path 跨平台归一化 ──
// 锁定:① 反斜杠→正斜杠 ② 小写 ③ 尾部分隔符裁剪 ④ 相同逻辑路径相等(防重复录入绕过)。
// 注:canonicalize 成功路径在不同 OS 返回不同形态(Windows 带前缀),故只断言跨平台不变性,
// 不断言精确串,避免与平台耦合。
#[test]
fn normalize_path_uses_forward_slash_and_lowercase() {
// 不存在的路径走降级分支(trim + replace + lowercase)
let n = normalize_path(r"C:\Foo\Bar\");
assert!(
!n.contains('\\'),
"反斜杠未归一: {n}"
);
assert_eq!(n, n.to_lowercase(), "未小写: {n}");
assert!(
!n.ends_with('/') && !n.ends_with('\\'),
"尾部分隔符未裁剪: {n}"
);
}
#[test]
fn normalize_path_equivalent_inputs_equal() {
// 两种写法应归一为同一串(去重场景)
let a = normalize_path(r"C:\Foo\Bar");
let b = normalize_path("C:/Foo/Bar/");
assert_eq!(a, b, "等价路径归一不等: {a} vs {b}");
}
#[test]
fn normalize_path_relative_is_lowercase_and_forward_slash() {
let n = normalize_path(r"..\Some\Path");
assert!(!n.contains('\\'), "反斜杠未归一: {n}");
assert_eq!(n, n.to_lowercase(), "未小写: {n}");
}
}