238 lines
9.2 KiB
Rust
238 lines
9.2 KiB
Rust
use anyhow::Result;
|
|
use wtismycode_core::{Config, ProjectModel, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
|
use colored::Colorize;
|
|
use indicatif::{ProgressBar, ProgressStyle};
|
|
use std::path::Path;
|
|
|
|
use crate::output::sanitize_filename;
|
|
|
|
pub fn load_config(config_path: &str) -> Result<Config> {
|
|
Config::load_from_file(Path::new(config_path))
|
|
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))
|
|
}
|
|
|
|
pub fn analyze_project(root: &str, config: &Config) -> Result<ProjectModel> {
|
|
println!("{}", "Scanning project...".cyan());
|
|
|
|
let scanner = FileScanner::new(config.clone());
|
|
let python_files = scanner.scan_python_files(std::path::Path::new(root))?;
|
|
|
|
println!(" Found {} Python files", python_files.len().to_string().yellow());
|
|
|
|
let analyzer = PythonAnalyzer::new(config.clone());
|
|
|
|
let pb = ProgressBar::new(python_files.len() as u64);
|
|
pb.set_style(ProgressStyle::default_bar()
|
|
.template(" {spinner:.green} [{bar:30.cyan/dim}] {pos}/{len} {msg}")
|
|
.unwrap_or_else(|_| ProgressStyle::default_bar())
|
|
.progress_chars("█▓░"));
|
|
|
|
let mut parsed_modules = Vec::new();
|
|
let mut parse_errors = 0;
|
|
for file_path in &python_files {
|
|
pb.set_message(file_path.file_name()
|
|
.map(|n| n.to_string_lossy().to_string())
|
|
.unwrap_or_default());
|
|
match analyzer.parse_module(file_path) {
|
|
Ok(module) => parsed_modules.push(module),
|
|
Err(e) => {
|
|
parse_errors += 1;
|
|
pb.println(format!(" {} Failed to parse {}: {}", "⚠".yellow(), file_path.display(), e));
|
|
}
|
|
}
|
|
pb.inc(1);
|
|
}
|
|
pb.finish_and_clear();
|
|
|
|
if parse_errors > 0 {
|
|
println!(" {} {} file(s) had parse errors", "⚠".yellow(), parse_errors);
|
|
}
|
|
|
|
println!("{}", "Resolving symbols...".cyan());
|
|
let model = analyzer.resolve_symbols(&parsed_modules)
|
|
.map_err(|e| anyhow::anyhow!("Failed to resolve symbols: {}", e))?;
|
|
|
|
Ok(model)
|
|
}
|
|
|
|
pub fn dry_run_docs(model: &ProjectModel, out: &str, config: &Config) -> Result<()> {
|
|
println!("{}", "Dry run — no files will be written.".cyan().bold());
|
|
println!();
|
|
|
|
let out_path = std::path::Path::new(out);
|
|
let arch_path = std::path::Path::new(".").join("ARCHITECTURE.md");
|
|
|
|
// ARCHITECTURE.md
|
|
let exists = arch_path.exists();
|
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, arch_path.display());
|
|
|
|
// layout.md
|
|
let layout_path = out_path.join("layout.md");
|
|
let exists = layout_path.exists();
|
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, layout_path.display());
|
|
|
|
// Module docs
|
|
for module_id in model.modules.keys() {
|
|
let p = out_path.join("modules").join(format!("{}.md", sanitize_filename(module_id)));
|
|
let exists = p.exists();
|
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, p.display());
|
|
}
|
|
|
|
// File docs
|
|
for file_doc in model.files.values() {
|
|
let p = out_path.join("files").join(format!("{}.md", sanitize_filename(&file_doc.path)));
|
|
let exists = p.exists();
|
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, p.display());
|
|
}
|
|
|
|
let _ = config; // used for future extensions
|
|
println!();
|
|
println!("{} {} file(s) would be generated/updated",
|
|
"✓".green().bold(),
|
|
2 + model.modules.len() + model.files.len());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool, _config: &Config) -> Result<()> {
|
|
println!("{}", "Generating documentation...".cyan());
|
|
|
|
let out_path = std::path::Path::new(out);
|
|
std::fs::create_dir_all(out_path)?;
|
|
|
|
let modules_path = out_path.join("modules");
|
|
let files_path = out_path.join("files");
|
|
std::fs::create_dir_all(&modules_path)?;
|
|
std::fs::create_dir_all(&files_path)?;
|
|
|
|
// Clean up stale files from previous runs
|
|
for subdir in &["modules", "files"] {
|
|
let dir = out_path.join(subdir);
|
|
if dir.exists()
|
|
&& let Ok(entries) = std::fs::read_dir(&dir) {
|
|
for entry in entries.flatten() {
|
|
if entry.path().extension().map(|e| e == "md").unwrap_or(false) {
|
|
let _ = std::fs::remove_file(entry.path());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let renderer = wtismycode_core::renderer::Renderer::new();
|
|
let writer = wtismycode_core::writer::DiffAwareWriter::new();
|
|
|
|
let output_path = std::path::Path::new(".").join("ARCHITECTURE.md");
|
|
|
|
// Generate module docs
|
|
for module_id in model.modules.keys() {
|
|
let module_doc_path = modules_path.join(format!("{}.md", sanitize_filename(module_id)));
|
|
if verbose {
|
|
println!(" Generating module doc: {}", module_id);
|
|
}
|
|
match renderer.render_module_md(model, module_id) {
|
|
Ok(module_content) => {
|
|
std::fs::write(&module_doc_path, module_content)?;
|
|
}
|
|
Err(e) => {
|
|
eprintln!(" {} Module {}: {}", "⚠".yellow(), module_id, e);
|
|
let fallback = format!("# Module: {}\n\nTODO: Add module documentation\n", module_id);
|
|
std::fs::write(&module_doc_path, fallback)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate file docs
|
|
for file_doc in model.files.values() {
|
|
if verbose {
|
|
println!(" Generating file doc: {}", file_doc.path);
|
|
}
|
|
let file_doc_path = files_path.join(format!("{}.md", sanitize_filename(&file_doc.path)));
|
|
|
|
let mut file_content = format!("# File: {}\n\n", file_doc.path);
|
|
file_content.push_str(&format!("- **Module:** {}\n", file_doc.module_id));
|
|
file_content.push_str(&format!("- **Defined symbols:** {}\n", file_doc.symbols.len()));
|
|
file_content.push_str(&format!("- **Imports:** {}\n\n", file_doc.imports.len()));
|
|
|
|
file_content.push_str("<!-- MANUAL:BEGIN -->\n## File intent (manual)\n<FILL_MANUALLY>\n<!-- MANUAL:END -->\n\n---\n\n");
|
|
|
|
file_content.push_str("## Imports & file-level dependencies\n<!-- ARCHDOC:BEGIN section=file_imports -->\n> Generated. Do not edit inside this block.\n");
|
|
for import in &file_doc.imports {
|
|
file_content.push_str(&format!("- {}\n", import));
|
|
}
|
|
file_content.push_str("<!-- ARCHDOC:END section=file_imports -->\n\n---\n\n");
|
|
|
|
file_content.push_str("## Symbols index\n<!-- ARCHDOC:BEGIN section=symbols_index -->\n> Generated. Do not edit inside this block.\n");
|
|
for symbol_id in &file_doc.symbols {
|
|
if let Some(symbol) = model.symbols.get(symbol_id) {
|
|
file_content.push_str(&format!("- `{}` ({:?})\n", symbol.qualname, symbol.kind));
|
|
}
|
|
}
|
|
file_content.push_str("<!-- ARCHDOC:END section=symbols_index -->\n\n---\n\n");
|
|
|
|
file_content.push_str("## Symbol details\n");
|
|
|
|
for symbol_id in &file_doc.symbols {
|
|
if model.symbols.contains_key(symbol_id) {
|
|
file_content.push_str(&format!("\n<!-- ARCHDOC:BEGIN symbol id={} -->\n", symbol_id));
|
|
file_content.push_str("<!-- AUTOGENERATED SYMBOL CONTENT WILL BE INSERTED HERE -->\n");
|
|
file_content.push_str(&format!("<!-- ARCHDOC:END symbol id={} -->\n", symbol_id));
|
|
}
|
|
}
|
|
|
|
std::fs::write(&file_doc_path, &file_content)?;
|
|
|
|
for symbol_id in &file_doc.symbols {
|
|
if model.symbols.contains_key(symbol_id) {
|
|
match renderer.render_symbol_details(model, symbol_id) {
|
|
Ok(content) => {
|
|
if verbose {
|
|
println!(" Updating symbol section for {}", symbol_id);
|
|
}
|
|
if let Err(e) = writer.update_symbol_section(&file_doc_path, symbol_id, &content) {
|
|
eprintln!(" {} Symbol {}: {}", "⚠".yellow(), symbol_id, e);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!(" {} Symbol {}: {}", "⚠".yellow(), symbol_id, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update ARCHITECTURE.md sections
|
|
let sections = [
|
|
("integrations", renderer.render_integrations_section(model)),
|
|
("rails", renderer.render_rails_section(model)),
|
|
("layout", renderer.render_layout_section(model)),
|
|
("modules_index", renderer.render_modules_index_section(model)),
|
|
("critical_points", renderer.render_critical_points_section(model)),
|
|
];
|
|
|
|
for (name, result) in sections {
|
|
match result {
|
|
Ok(content) => {
|
|
if let Err(e) = writer.update_file_with_markers(&output_path, &content, name)
|
|
&& verbose {
|
|
eprintln!(" {} Section {}: {}", "⚠".yellow(), name, e);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if verbose {
|
|
eprintln!(" {} Section {}: {}", "⚠".yellow(), name, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update layout.md
|
|
let layout_md_path = out_path.join("layout.md");
|
|
if let Ok(content) = renderer.render_layout_md(model) {
|
|
let _ = std::fs::write(&layout_md_path, &content);
|
|
}
|
|
|
|
println!("{} Documentation generated in {}", "✓".green().bold(), out);
|
|
|
|
Ok(())
|
|
}
|