use std::path::Path;
use colored::Colorize;
use sem_core::model::entity::SemanticEntity;
use sem_core::parser::graph::EntityGraph;
use sem_core::parser::registry::ParserRegistry;
use crate::cache::DiskCache;
pub struct GraphOptions {
pub cwd: String,
pub json: bool,
pub file_exts: Vec<String>,
pub no_cache: bool,
}
pub fn graph_command(opts: GraphOptions) {
let root = Path::new(&opts.cwd);
let registry = super::create_registry(&opts.cwd);
let ext_filter = normalize_exts(&opts.file_exts);
let file_paths = find_supported_files_public(root, ®istry, &ext_filter);
let (graph, _entities) = get_or_build_graph(root, &file_paths, ®istry, opts.no_cache);
if opts.json {
let output = serde_json::json!({
"entities": graph.entities.values().collect::<Vec<_>>(),
"edges": &graph.edges,
"stats": {
"entityCount": graph.entities.len(),
"edgeCount": graph.edges.len()
}
});
println!("{}", serde_json::to_string(&output).unwrap());
} else {
println!(
"{} {} entities, {} edges",
"⊕".green(),
graph.entities.len().to_string().bold(),
graph.edges.len().to_string().bold(),
);
}
}
pub fn normalize_exts(exts: &[String]) -> Vec<String> {
exts.iter().map(|e| {
if e.starts_with('.') { e.clone() } else { format!(".{}", e) }
}).collect()
}
pub fn find_supported_files_public(root: &Path, registry: &ParserRegistry, ext_filter: &[String]) -> Vec<String> {
find_supported_files(root, registry, ext_filter)
}
const DEFAULT_EXCLUDED_FILES: &[&str] = &[
"Cargo.lock",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"Gemfile.lock",
"Pipfile.lock",
"poetry.lock",
"composer.lock",
"go.sum",
"flake.lock",
];
const DEFAULT_EXCLUDED_DIRS: &[&str] = &[
"fixtures",
"fixture",
"benchmarks",
"vendor",
"node_modules",
"test-harness",
];
fn is_default_excluded(rel_path: &str) -> bool {
if let Some(file_name) = rel_path.rsplit('/').next() {
if DEFAULT_EXCLUDED_FILES.contains(&file_name) {
return true;
}
}
for component in rel_path.split('/') {
if DEFAULT_EXCLUDED_DIRS.contains(&component) {
return true;
}
}
false
}
fn find_supported_files(root: &Path, registry: &ParserRegistry, ext_filter: &[String]) -> Vec<String> {
let mut files = Vec::new();
let mut builder = ignore::WalkBuilder::new(root);
builder
.hidden(true) .git_ignore(true) .git_global(true) .git_exclude(true);
let semignore = root.join(".semignore");
if semignore.exists() {
builder.add_ignore(semignore);
}
let walker = builder.build();
for entry in walker.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Ok(rel) = path.strip_prefix(root) {
let rel_str = rel.to_string_lossy().to_string();
if is_default_excluded(&rel_str) {
continue;
}
if !ext_filter.is_empty() && !ext_filter.iter().any(|ext| rel_str.ends_with(ext.as_str())) {
continue;
}
if registry.get_plugin(&rel_str).is_some() {
files.push(rel_str);
}
}
}
files.sort();
files
}
pub fn get_or_build_graph(
root: &Path,
file_paths: &[String],
registry: &ParserRegistry,
no_cache: bool,
) -> (EntityGraph, Vec<SemanticEntity>) {
if !no_cache {
if let Ok(disk) = DiskCache::open(root) {
if let Some(cached) = disk.load(root, file_paths) {
return cached;
}
if let Some(partial) = disk.load_partial(root, file_paths) {
let (graph, entities) = EntityGraph::build_incremental(
root,
&partial.stale_files,
file_paths,
partial.cached_entities,
partial.cached_edges,
partial.stale_file_entities,
registry,
);
let _ = disk.save_incremental(
root,
file_paths,
&partial.stale_files,
&graph,
&entities,
);
return (graph, entities);
}
}
}
let (graph, entities) = EntityGraph::build(root, file_paths, registry);
if !no_cache {
if let Ok(disk) = DiskCache::open(root) {
let _ = disk.save(root, file_paths, &graph, &entities);
}
}
(graph, entities)
}