746 lines
26 KiB
Rust
746 lines
26 KiB
Rust
use std::collections::{HashMap, HashSet};
|
||
use std::path::{Path, PathBuf};
|
||
use std::time::Instant;
|
||
|
||
use tauri::Emitter;
|
||
|
||
use crate::types::{
|
||
Action, ActionKind, AnalyzeReport, Finding, LlmContext, ProjectContext, ProjectSignal,
|
||
ProjectStructure, Recommendation, ReportStats,
|
||
};
|
||
|
||
const MAX_FILES: u64 = 50_000;
|
||
const MAX_DURATION_SECS: u64 = 60;
|
||
const TOP_EXTENSIONS_N: usize = 15;
|
||
const MAX_DEPTH_WARN: u32 = 6;
|
||
const ROOT_FILES_WARN: u64 = 20;
|
||
|
||
const EXCLUDED_DIRS: &[&str] = &[
|
||
".git", "node_modules", "dist", "build", ".next", "target", ".cache", "coverage",
|
||
];
|
||
|
||
const MARKER_README: &[&str] = &["README", "readme", "Readme"];
|
||
const MARKER_VITE: &[&str] = &["vite.config.js", "vite.config.ts", "vite.config.mjs"];
|
||
|
||
#[derive(Default)]
|
||
struct ScanState {
|
||
file_count: u64,
|
||
dir_count: u64,
|
||
total_size_bytes: u64,
|
||
extensions: HashMap<String, u64>,
|
||
has_readme: bool,
|
||
has_package_json: bool,
|
||
has_cargo_toml: bool,
|
||
has_env: bool,
|
||
has_docker: bool,
|
||
has_tsconfig: bool,
|
||
has_vite: bool,
|
||
has_next: bool,
|
||
has_gitignore: bool,
|
||
has_license: bool,
|
||
has_eslint: bool,
|
||
has_prettier: bool,
|
||
has_tests_dir: bool,
|
||
has_src: bool,
|
||
has_components: bool,
|
||
has_pages: bool,
|
||
has_requirements_txt: bool,
|
||
has_pyproject: bool,
|
||
has_setup_py: bool,
|
||
package_json_count: u32,
|
||
cargo_toml_count: u32,
|
||
root_file_count: u64,
|
||
root_dirs: HashSet<String>,
|
||
max_depth: u32,
|
||
}
|
||
|
||
const PROGRESS_EVENT: &str = "analyze_progress";
|
||
|
||
#[tauri::command]
|
||
pub fn analyze_project(window: tauri::Window, path: String) -> Result<AnalyzeReport, String> {
|
||
let root = PathBuf::from(&path);
|
||
if !root.exists() {
|
||
return Err("Путь не существует".to_string());
|
||
}
|
||
if !root.is_dir() {
|
||
return Err("Путь не является папкой".to_string());
|
||
}
|
||
|
||
let _ = window.emit(PROGRESS_EVENT, "Сканирую структуру…");
|
||
|
||
let deadline = Instant::now() + std::time::Duration::from_secs(MAX_DURATION_SECS);
|
||
let mut state = ScanState::default();
|
||
|
||
scan_dir(&root, &root, 0, &mut state, &deadline)?;
|
||
|
||
let _ = window.emit(PROGRESS_EVENT, "Анализирую архитектуру…");
|
||
|
||
let top_extensions: Vec<(String, u64)> = {
|
||
let mut v: Vec<_> = state.extensions.iter().map(|(k, v)| (k.clone(), *v)).collect();
|
||
v.sort_by(|a, b| b.1.cmp(&a.1));
|
||
v.into_iter().take(TOP_EXTENSIONS_N).collect()
|
||
};
|
||
|
||
let stats = ReportStats {
|
||
file_count: state.file_count,
|
||
dir_count: state.dir_count,
|
||
total_size_bytes: state.total_size_bytes,
|
||
top_extensions,
|
||
max_depth: state.max_depth as u64,
|
||
};
|
||
|
||
let structure = build_structure(&state);
|
||
let mut findings: Vec<Finding> = Vec::new();
|
||
let mut recommendations: Vec<Recommendation> = Vec::new();
|
||
let mut signals: Vec<ProjectSignal> = Vec::new();
|
||
|
||
if state.has_env {
|
||
findings.push(Finding {
|
||
severity: "high".to_string(),
|
||
title: "Риск секретов".to_string(),
|
||
details: "Обнаружены файлы .env или .env.* — не коммитьте секреты в репозиторий.".to_string(),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "security".to_string(),
|
||
level: "high".to_string(),
|
||
message: "Есть .env файл — риск утечки секретов.".to_string(),
|
||
});
|
||
}
|
||
|
||
if !state.has_readme {
|
||
recommendations.push(Recommendation {
|
||
title: "Добавить README".to_string(),
|
||
details: "Опишите проект и инструкцию по запуску.".to_string(),
|
||
priority: "medium".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "high".to_string(),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "quality".to_string(),
|
||
level: "warn".to_string(),
|
||
message: "Нет README.".to_string(),
|
||
});
|
||
}
|
||
|
||
if !state.has_gitignore {
|
||
recommendations.push(Recommendation {
|
||
title: "Добавить .gitignore".to_string(),
|
||
details: "Исключите артефакты сборки и зависимости из репозитория.".to_string(),
|
||
priority: "medium".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "medium".to_string(),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "quality".to_string(),
|
||
level: "warn".to_string(),
|
||
message: "Нет .gitignore.".to_string(),
|
||
});
|
||
}
|
||
|
||
if !state.has_license {
|
||
recommendations.push(Recommendation {
|
||
title: "Указать лицензию".to_string(),
|
||
details: "Добавьте LICENSE или LICENSE.md.".to_string(),
|
||
priority: "low".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "medium".to_string(),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "quality".to_string(),
|
||
level: "info".to_string(),
|
||
message: "Нет файла лицензии.".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.has_src && !state.has_tests_dir {
|
||
recommendations.push(Recommendation {
|
||
title: "Добавить тесты".to_string(),
|
||
details: "Есть src/, но нет папки tests/ — добавьте базовые тесты.".to_string(),
|
||
priority: "high".to_string(),
|
||
effort: "medium".to_string(),
|
||
impact: "high".to_string(),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "structure".to_string(),
|
||
level: "warn".to_string(),
|
||
message: "Есть src/, нет tests/.".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.has_components && !state.has_pages && state.has_package_json {
|
||
recommendations.push(Recommendation {
|
||
title: "Проверить структуру фронтенда".to_string(),
|
||
details: "Есть components/, но нет pages/ — возможно, маршруты или страницы в другом месте.".to_string(),
|
||
priority: "low".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "low".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.root_file_count >= ROOT_FILES_WARN {
|
||
findings.push(Finding {
|
||
severity: "warn".to_string(),
|
||
title: "Много файлов в корне".to_string(),
|
||
details: format!("В корне {} файлов — рассмотрите группировку по папкам.", state.root_file_count),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "structure".to_string(),
|
||
level: "warn".to_string(),
|
||
message: "Слишком много файлов в корне проекта.".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.max_depth >= MAX_DEPTH_WARN {
|
||
findings.push(Finding {
|
||
severity: "warn".to_string(),
|
||
title: "Глубокая вложенность".to_string(),
|
||
details: format!("Вложенность до {} уровней — усложняет навигацию.", state.max_depth),
|
||
});
|
||
signals.push(ProjectSignal {
|
||
category: "structure".to_string(),
|
||
level: "warn".to_string(),
|
||
message: "Глубокая вложенность папок.".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.has_package_json && !state.has_eslint && !state.has_cargo_toml {
|
||
recommendations.push(Recommendation {
|
||
title: "Добавить линтер".to_string(),
|
||
details: "Рекомендуется ESLint (и при необходимости Prettier) для JavaScript/TypeScript.".to_string(),
|
||
priority: "medium".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "medium".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.has_cargo_toml && !state.has_eslint {
|
||
recommendations.push(Recommendation {
|
||
title: "Использовать Clippy".to_string(),
|
||
details: "Добавьте в CI или pre-commit: cargo clippy.".to_string(),
|
||
priority: "medium".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "medium".to_string(),
|
||
});
|
||
}
|
||
|
||
if !state.has_package_json && !state.has_cargo_toml && !state.has_pyproject && !state.has_requirements_txt {
|
||
findings.push(Finding {
|
||
severity: "warn".to_string(),
|
||
title: "Неопределён тип проекта".to_string(),
|
||
details: "Не найдены привычные манифесты (package.json, Cargo.toml, pyproject.toml).".to_string(),
|
||
});
|
||
}
|
||
|
||
if state.file_count > 30_000 || state.dir_count > 5_000 {
|
||
recommendations.push(Recommendation {
|
||
title: "Сузить область анализа".to_string(),
|
||
details: "Очень много файлов или папок — добавьте исключения или выберите подпапку.".to_string(),
|
||
priority: "medium".to_string(),
|
||
effort: "low".to_string(),
|
||
impact: "low".to_string(),
|
||
});
|
||
}
|
||
|
||
let _ = window.emit(PROGRESS_EVENT, "Формирую вывод…");
|
||
|
||
let recommendations = enrich_recommendations(recommendations);
|
||
let project_context = build_project_context(&state, &findings, &signals);
|
||
let actions = build_actions(state.has_readme, state.has_tests_dir, state.has_gitignore);
|
||
|
||
let narrative = build_narrative(&state, &structure, &findings, &recommendations);
|
||
|
||
let report = AnalyzeReport {
|
||
path: path.clone(),
|
||
narrative: narrative.clone(),
|
||
stats: stats.clone(),
|
||
structure: structure.clone(),
|
||
project_context: project_context.clone(),
|
||
findings: findings.clone(),
|
||
recommendations: recommendations.clone(),
|
||
actions: actions.clone(),
|
||
signals: signals.clone(),
|
||
report_md: String::new(),
|
||
llm_context: LlmContext {
|
||
concise_summary: String::new(),
|
||
key_risks: Vec::new(),
|
||
top_recommendations: Vec::new(),
|
||
signals: Vec::new(),
|
||
},
|
||
};
|
||
let report_md = build_markdown_report(&report);
|
||
let llm_context = build_llm_context(&report);
|
||
|
||
Ok(AnalyzeReport {
|
||
path: path.clone(),
|
||
narrative: report.narrative,
|
||
stats: report.stats,
|
||
structure: report.structure,
|
||
project_context: report.project_context,
|
||
findings: report.findings,
|
||
recommendations: report.recommendations,
|
||
actions: report.actions,
|
||
signals: report.signals,
|
||
report_md,
|
||
llm_context,
|
||
})
|
||
}
|
||
|
||
fn build_actions(has_readme: bool, has_tests: bool, has_gitignore: bool) -> Vec<Action> {
|
||
let mut actions = vec![];
|
||
|
||
if !has_readme {
|
||
actions.push(Action {
|
||
id: "add-readme".into(),
|
||
title: "Добавить README.md".into(),
|
||
description: "В проекте отсутствует README.md".into(),
|
||
kind: ActionKind::CreateFile,
|
||
path: "README.md".into(),
|
||
content: Some("# Project\n\nDescribe your project.\n".into()),
|
||
});
|
||
}
|
||
|
||
if !has_tests {
|
||
actions.push(Action {
|
||
id: "add-tests-dir".into(),
|
||
title: "Создать папку tests/".into(),
|
||
description: "В проекте нет tests/ (минимальная структура для тестов)".into(),
|
||
kind: ActionKind::CreateDir,
|
||
path: "tests".into(),
|
||
content: None,
|
||
});
|
||
}
|
||
|
||
if !has_gitignore {
|
||
actions.push(Action {
|
||
id: "add-gitignore".into(),
|
||
title: "Добавить .gitignore".into(),
|
||
description: "Рекомендуется добавить базовый .gitignore".into(),
|
||
kind: ActionKind::CreateFile,
|
||
path: ".gitignore".into(),
|
||
content: Some("node_modules/\ndist/\nbuild/\n.target/\n.DS_Store\n".into()),
|
||
});
|
||
}
|
||
|
||
actions
|
||
}
|
||
|
||
fn enrich_recommendations(mut recs: Vec<Recommendation>) -> Vec<Recommendation> {
|
||
for r in &mut recs {
|
||
let (p, e, i) = if r.title.contains("README") {
|
||
("high", "low", "high")
|
||
} else if r.title.contains("тест") || r.title.contains("тесты") || r.title.contains("Add tests") || r.title.contains("tests") {
|
||
("high", "medium", "high")
|
||
} else if r.title.contains(".gitignore") {
|
||
("medium", "low", "medium")
|
||
} else if r.title.contains(".env") || r.title.contains("секрет") {
|
||
("high", "low", "high")
|
||
} else if r.title.contains("лицензи") {
|
||
("low", "low", "medium")
|
||
} else if r.title.contains("линтер") || r.title.contains("Clippy") {
|
||
("medium", "low", "medium")
|
||
} else {
|
||
(r.priority.as_str(), r.effort.as_str(), r.impact.as_str())
|
||
};
|
||
r.priority = p.to_string();
|
||
r.effort = e.to_string();
|
||
r.impact = i.to_string();
|
||
}
|
||
recs
|
||
}
|
||
|
||
fn build_project_context(
|
||
state: &ScanState,
|
||
findings: &[Finding],
|
||
signals: &[ProjectSignal],
|
||
) -> ProjectContext {
|
||
let risk_level = if state.has_env
|
||
|| signals.iter().any(|s| s.category == "security" && s.level == "high")
|
||
{
|
||
"High"
|
||
} else if findings.len() > 5
|
||
|| signals.iter().any(|s| s.level == "warn")
|
||
{
|
||
"Medium"
|
||
} else {
|
||
"Low"
|
||
};
|
||
|
||
let complexity = if state.file_count > 5000 || state.dir_count > 500 || state.max_depth > 8 {
|
||
"High"
|
||
} else if state.file_count > 800 || state.dir_count > 120 {
|
||
"Medium"
|
||
} else {
|
||
"Low"
|
||
};
|
||
|
||
let maturity = if state.has_readme && (state.has_tests_dir || state.has_eslint) {
|
||
"Production-like"
|
||
} else if state.has_readme {
|
||
"MVP"
|
||
} else {
|
||
"Prototype"
|
||
};
|
||
|
||
let mut stack = Vec::new();
|
||
if state.has_package_json {
|
||
stack.push("Node.js".to_string());
|
||
}
|
||
if state.has_cargo_toml {
|
||
stack.push("Rust".to_string());
|
||
}
|
||
if state.has_vite {
|
||
stack.push("Vite".to_string());
|
||
}
|
||
if state.has_next {
|
||
stack.push("Next.js".to_string());
|
||
}
|
||
if state.has_pyproject || state.has_requirements_txt {
|
||
stack.push("Python".to_string());
|
||
}
|
||
if stack.is_empty() {
|
||
stack.push("Unknown".to_string());
|
||
}
|
||
|
||
let domain = if state.has_next || state.has_vite {
|
||
"frontend"
|
||
} else if state.has_cargo_toml {
|
||
"systems"
|
||
} else if state.has_package_json {
|
||
"fullstack"
|
||
} else {
|
||
"general"
|
||
}
|
||
.to_string();
|
||
|
||
ProjectContext {
|
||
stack,
|
||
domain,
|
||
maturity: maturity.to_string(),
|
||
complexity: complexity.to_string(),
|
||
risk_level: risk_level.to_string(),
|
||
}
|
||
}
|
||
|
||
fn build_markdown_report(report: &AnalyzeReport) -> String {
|
||
let mut md = String::new();
|
||
md.push_str("# PAPA YU — отчёт анализа проекта\n\n");
|
||
md.push_str(&report.narrative);
|
||
md.push_str("\n\n---\n\n");
|
||
md.push_str("## Статистика\n\n");
|
||
md.push_str(&format!(
|
||
"- Файлов: {}\n- Папок: {}\n- Max depth: {}\n- Размер: {} Б\n\n",
|
||
report.stats.file_count,
|
||
report.stats.dir_count,
|
||
report.stats.max_depth,
|
||
report.stats.total_size_bytes
|
||
));
|
||
md.push_str("## Контекст проекта\n\n");
|
||
md.push_str(&format!(
|
||
"- Стек: {}\n- Зрелость: {}\n- Сложность: {}\n- Риск: {}\n\n",
|
||
report.project_context.stack.join(", "),
|
||
report.project_context.maturity,
|
||
report.project_context.complexity,
|
||
report.project_context.risk_level
|
||
));
|
||
if !report.findings.is_empty() {
|
||
md.push_str("## Находки\n\n");
|
||
for f in &report.findings {
|
||
md.push_str(&format!("- **{}**: {}\n", f.title, f.details));
|
||
}
|
||
md.push_str("\n");
|
||
}
|
||
if !report.recommendations.is_empty() {
|
||
md.push_str("## Рекомендации\n\n");
|
||
for r in &report.recommendations {
|
||
md.push_str(&format!(
|
||
"- **{}** [{} / effort:{} / impact:{}]\n {}\n",
|
||
r.title, r.priority, r.effort, r.impact, r.details
|
||
));
|
||
}
|
||
}
|
||
md
|
||
}
|
||
|
||
fn build_llm_context(report: &AnalyzeReport) -> LlmContext {
|
||
let concise_summary = format!(
|
||
"{}; {}; {} файлов, {} папок. Риск: {}, зрелость: {}.",
|
||
report.structure.project_type,
|
||
report.structure.architecture,
|
||
report.stats.file_count,
|
||
report.stats.dir_count,
|
||
report.project_context.risk_level,
|
||
report.project_context.maturity
|
||
);
|
||
let key_risks: Vec<String> = report
|
||
.findings
|
||
.iter()
|
||
.filter(|f| f.severity == "high")
|
||
.map(|f| format!("{}: {}", f.title, f.details))
|
||
.collect();
|
||
let top_recommendations: Vec<String> = report
|
||
.recommendations
|
||
.iter()
|
||
.take(5)
|
||
.map(|r| format!("[{}] {}", r.priority, r.title))
|
||
.collect();
|
||
LlmContext {
|
||
concise_summary,
|
||
key_risks,
|
||
top_recommendations,
|
||
signals: report.signals.clone(),
|
||
}
|
||
}
|
||
|
||
fn build_structure(state: &ScanState) -> ProjectStructure {
|
||
let mut project_type = String::new();
|
||
let mut architecture = String::new();
|
||
let mut structure_notes: Vec<String> = Vec::new();
|
||
|
||
let is_monorepo = state.package_json_count > 1 || state.cargo_toml_count > 1;
|
||
|
||
if state.has_cargo_toml {
|
||
project_type = "Rust / Cargo".to_string();
|
||
architecture = "Rust-проект".to_string();
|
||
if is_monorepo {
|
||
project_type = "Rust monorepo".to_string();
|
||
}
|
||
}
|
||
if state.has_package_json {
|
||
if !project_type.is_empty() {
|
||
project_type = format!("{} + Node", project_type);
|
||
} else if state.has_next {
|
||
project_type = "Next.js".to_string();
|
||
architecture = "React (Next.js) fullstack".to_string();
|
||
} else if state.has_vite {
|
||
project_type = "React + Vite".to_string();
|
||
architecture = "Frontend SPA (Vite)".to_string();
|
||
} else {
|
||
project_type = "Node.js".to_string();
|
||
architecture = "Node / frontend или backend".to_string();
|
||
}
|
||
if is_monorepo && !project_type.contains("monorepo") {
|
||
project_type = format!("{} (monorepo)", project_type);
|
||
}
|
||
}
|
||
if state.has_pyproject || state.has_requirements_txt || state.has_setup_py {
|
||
if !project_type.is_empty() {
|
||
project_type = format!("{} + Python", project_type);
|
||
} else {
|
||
project_type = "Python".to_string();
|
||
architecture = "Python-проект (Django/FastAPI или скрипты)".to_string();
|
||
}
|
||
}
|
||
if project_type.is_empty() {
|
||
project_type = "Неопределён".to_string();
|
||
architecture = "Тип по манифестам не определён".to_string();
|
||
}
|
||
|
||
if state.has_src && state.has_tests_dir {
|
||
structure_notes.push("Есть src/ и tests/ — хорошее разделение.".to_string());
|
||
} else if state.has_src && !state.has_tests_dir {
|
||
structure_notes.push("Есть src/, нет tests/ — стоит добавить тесты.".to_string());
|
||
}
|
||
if state.root_file_count >= ROOT_FILES_WARN {
|
||
structure_notes.push("Много файлов в корне — структура упрощённая.".to_string());
|
||
}
|
||
if state.max_depth >= MAX_DEPTH_WARN {
|
||
structure_notes.push("Глубокая вложенность папок.".to_string());
|
||
}
|
||
if structure_notes.is_empty() {
|
||
structure_notes.push("Структура без явного разделения на домены.".to_string());
|
||
}
|
||
|
||
ProjectStructure {
|
||
project_type,
|
||
architecture,
|
||
structure_notes,
|
||
}
|
||
}
|
||
|
||
fn build_narrative(
|
||
state: &ScanState,
|
||
structure: &ProjectStructure,
|
||
findings: &[Finding],
|
||
recommendations: &[Recommendation],
|
||
) -> String {
|
||
let mut parts = Vec::new();
|
||
parts.push("Я проанализировал ваш проект.".to_string());
|
||
parts.push(format!(
|
||
"Это {} ({}).",
|
||
structure.project_type.to_lowercase(),
|
||
structure.architecture
|
||
));
|
||
if !structure.structure_notes.is_empty() {
|
||
parts.push(structure.structure_notes.join(" "));
|
||
}
|
||
|
||
let size_label = if state.file_count < 50 {
|
||
"небольшой"
|
||
} else if state.file_count < 500 {
|
||
"среднего размера"
|
||
} else {
|
||
"крупный"
|
||
};
|
||
parts.push(format!(
|
||
"По размеру — {} проект: {} файлов, {} папок.",
|
||
size_label, state.file_count, state.dir_count
|
||
));
|
||
|
||
if !findings.is_empty() {
|
||
parts.push("".to_string());
|
||
parts.push("Основные проблемы:".to_string());
|
||
for f in findings.iter().take(7) {
|
||
parts.push(format!("– {}", f.title));
|
||
}
|
||
}
|
||
|
||
if !recommendations.is_empty() {
|
||
parts.push("".to_string());
|
||
parts.push("Я бы рекомендовал начать с:".to_string());
|
||
for (i, r) in recommendations.iter().take(5).enumerate() {
|
||
parts.push(format!("{}. {}", i + 1, r.title));
|
||
}
|
||
}
|
||
|
||
parts.join("\n\n")
|
||
}
|
||
|
||
fn scan_dir(
|
||
root: &Path,
|
||
dir: &Path,
|
||
depth: u32,
|
||
state: &mut ScanState,
|
||
deadline: &Instant,
|
||
) -> Result<(), String> {
|
||
if Instant::now() > *deadline {
|
||
return Err("Превышено время анализа (таймаут)".to_string());
|
||
}
|
||
if state.file_count >= MAX_FILES {
|
||
return Err("Превышен лимит количества файлов".to_string());
|
||
}
|
||
|
||
if depth > state.max_depth {
|
||
state.max_depth = depth;
|
||
}
|
||
|
||
let is_root = dir == root;
|
||
|
||
let entries = match std::fs::read_dir(dir) {
|
||
Ok(e) => e,
|
||
Err(_) => return Ok(()),
|
||
};
|
||
|
||
for entry in entries.flatten() {
|
||
let path = entry.path();
|
||
let meta = match entry.metadata() {
|
||
Ok(m) => m,
|
||
Err(_) => continue,
|
||
};
|
||
|
||
if meta.is_symlink() {
|
||
continue;
|
||
}
|
||
|
||
if meta.is_dir() {
|
||
state.dir_count += 1;
|
||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||
if is_root {
|
||
state.root_dirs.insert(name.to_lowercase());
|
||
}
|
||
let name_lower = name.to_lowercase();
|
||
if EXCLUDED_DIRS.contains(&name) {
|
||
continue;
|
||
}
|
||
if name_lower == "src" {
|
||
state.has_src = true;
|
||
}
|
||
if name_lower == "tests" || name_lower == "test" || name_lower == "__tests__" {
|
||
state.has_tests_dir = true;
|
||
}
|
||
if name_lower == "components" {
|
||
state.has_components = true;
|
||
}
|
||
if name_lower == "pages" || name_lower == "app" {
|
||
state.has_pages = true;
|
||
}
|
||
scan_dir(root, &path, depth + 1, state, deadline)?;
|
||
continue;
|
||
}
|
||
|
||
state.file_count += 1;
|
||
if state.file_count >= MAX_FILES {
|
||
return Err("Превышен лимит количества файлов".to_string());
|
||
}
|
||
|
||
if is_root {
|
||
state.root_file_count += 1;
|
||
}
|
||
|
||
state.total_size_bytes = state.total_size_bytes.saturating_add(meta.len());
|
||
|
||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||
let name_lower = name.to_lowercase();
|
||
if let Some(ext) = path.extension() {
|
||
let ext = ext.to_string_lossy().to_string();
|
||
*state.extensions.entry(ext).or_insert(0) += 1;
|
||
}
|
||
|
||
if name_lower == "package.json" {
|
||
state.has_package_json = true;
|
||
state.package_json_count += 1;
|
||
}
|
||
if name_lower == "cargo.toml" {
|
||
state.has_cargo_toml = true;
|
||
state.cargo_toml_count += 1;
|
||
}
|
||
if name_lower == "tsconfig.json" {
|
||
state.has_tsconfig = true;
|
||
}
|
||
if name_lower == "dockerfile" || name_lower == "docker-compose.yml" {
|
||
state.has_docker = true;
|
||
}
|
||
if name_lower.starts_with(".env") {
|
||
state.has_env = true;
|
||
}
|
||
if name_lower == ".gitignore" {
|
||
state.has_gitignore = true;
|
||
}
|
||
if name_lower == "license" || name_lower == "license.md" || name_lower.starts_with("license.") {
|
||
state.has_license = true;
|
||
}
|
||
if name_lower == "eslint.config.js" || name_lower == ".eslintrc" || name_lower.starts_with(".eslintrc") {
|
||
state.has_eslint = true;
|
||
}
|
||
if name_lower == ".prettierrc" || name_lower == "prettier.config" || name_lower.starts_with("prettier.config") {
|
||
state.has_prettier = true;
|
||
}
|
||
if name_lower == "next.config.js" || name_lower == "next.config.mjs" || name_lower == "next.config.ts" {
|
||
state.has_next = true;
|
||
}
|
||
if name_lower == "requirements.txt" {
|
||
state.has_requirements_txt = true;
|
||
}
|
||
if name_lower == "pyproject.toml" {
|
||
state.has_pyproject = true;
|
||
}
|
||
if name_lower == "setup.py" {
|
||
state.has_setup_py = true;
|
||
}
|
||
for m in MARKER_README {
|
||
if name_lower.starts_with(&m.to_lowercase()) {
|
||
state.has_readme = true;
|
||
break;
|
||
}
|
||
}
|
||
for m in MARKER_VITE {
|
||
if name_lower == *m {
|
||
state.has_vite = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|