use crate::model::Symbol;
use crate::output::OutputFormat;
use crate::pipeline::GraphAnalysis;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct EmitConfig {
pub output: Option<PathBuf>,
pub format: OutputFormat,
pub html: bool,
pub open_browser: bool,
}
pub fn emit_inspect(symbols: &mut Vec<Symbol>, config: &EmitConfig) -> anyhow::Result<()> {
let content = crate::output::inspect::serialize_inspect(symbols, &config.format)?;
match &config.output {
Some(path) => {
crate::output::write_atomic(path, content.as_bytes())?;
}
None => {
println!("{content}");
}
}
Ok(())
}
pub fn emit_graph(analysis: &GraphAnalysis, config: &EmitConfig) -> anyhow::Result<()> {
if config.html {
let html = crate::output::dashboard::to_graph_html(
&analysis.graph,
&analysis.scc,
analysis.snapshot_id.to_raw() as u64,
)?;
match &config.output {
Some(path) => {
crate::output::write_atomic(path, html.as_bytes())?;
if config.open_browser {
open_in_browser(path)?;
}
}
None => println!("{html}"),
}
} else {
let content = crate::output::graph::serialize_graph(
&analysis.graph,
&analysis.scc,
analysis.snapshot_id.to_raw() as u64,
&config.format,
)?;
match &config.output {
Some(path) => {
crate::output::write_atomic(path, content.as_bytes())?;
}
None => {
println!("{content}");
}
}
}
Ok(())
}
fn open_in_browser(path: &std::path::Path) -> anyhow::Result<()> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
match url::Url::from_file_path(&absolute) {
Ok(url) => {
if let Err(e) = webbrowser::open(url.as_str()) {
tracing::warn!(error = %e, "could not open browser");
}
}
Err(()) => tracing::warn!(
path = %absolute.display(),
"cannot open the dashboard in a browser"
),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{GraphBuilder, SccAnalysis};
use crate::language::LangId;
use crate::model::{LineColumn, SnapshotId, SourceRange, Symbol, SymbolId, SymbolKind};
use crate::output::OutputFormat;
#[test]
fn emit_inspect_writes_to_path() {
let temp_dir = std::env::temp_dir().join("emit_inspect_writes_to_path");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("output.json");
let mut symbols = vec![Symbol {
id: SymbolId::new(1).unwrap(),
name: "test".into(),
kind: SymbolKind::Function,
language: LangId::Python,
file_path: PathBuf::from("a.py"),
source_range: SourceRange {
byte_start: 0,
byte_end: 10,
start: LineColumn { line: 1, column: 0 },
end: LineColumn {
line: 1,
column: 10,
},
},
name_range: None,
visibility: None,
signature: None,
docstring: None,
is_async: false,
}];
let config = EmitConfig {
output: Some(file_path.clone()),
format: OutputFormat::Json,
html: false,
open_browser: false,
};
emit_inspect(&mut symbols, &config).unwrap();
assert!(file_path.exists());
let content = std::fs::read_to_string(file_path).unwrap();
assert!(content.contains("test"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn emit_inspect_prints_to_stdout_when_no_path() {
let mut symbols = vec![Symbol {
id: SymbolId::new(1).unwrap(),
name: "test".into(),
kind: SymbolKind::Function,
language: LangId::Python,
file_path: PathBuf::from("a.py"),
source_range: SourceRange {
byte_start: 0,
byte_end: 10,
start: LineColumn { line: 1, column: 0 },
end: LineColumn {
line: 1,
column: 10,
},
},
name_range: None,
visibility: None,
signature: None,
docstring: None,
is_async: false,
}];
let config = EmitConfig {
output: None,
format: OutputFormat::Json,
html: false,
open_browser: false,
};
emit_inspect(&mut symbols, &config).unwrap();
}
#[test]
fn emit_graph_html_writes_file() {
let temp_dir = std::env::temp_dir().join("emit_graph_html_writes_file");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("graph.html");
let builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
let graph = builder.build();
let scc = SccAnalysis::analyze(graph.graph());
let analysis = GraphAnalysis {
graph,
scc,
snapshot_id: SnapshotId::new(1).unwrap(),
extractions: vec![],
scope: crate::graph::resolver::FlattenedScopeCache::default(),
references: Vec::new(),
};
let config = EmitConfig {
output: Some(file_path.clone()),
format: OutputFormat::Json,
html: true,
open_browser: false,
};
emit_graph(&analysis, &config).unwrap();
assert!(file_path.exists());
let content = std::fs::read_to_string(file_path).unwrap();
assert!(content.contains("<!DOCTYPE html>"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn emit_graph_text_json_output() {
let temp_dir = std::env::temp_dir().join("emit_graph_text_json_output");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("graph.json");
let builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
let graph = builder.build();
let scc = SccAnalysis::analyze(graph.graph());
let analysis = GraphAnalysis {
graph,
scc,
snapshot_id: SnapshotId::new(1).unwrap(),
extractions: vec![],
scope: crate::graph::resolver::FlattenedScopeCache::default(),
references: Vec::new(),
};
let config = EmitConfig {
output: Some(file_path.clone()),
format: OutputFormat::Json,
html: false,
open_browser: false,
};
emit_graph(&analysis, &config).unwrap();
assert!(file_path.exists());
let content = std::fs::read_to_string(file_path).unwrap();
assert!(content.contains("nodes"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
}