pub mod cli;
pub mod model;
pub mod remote;
pub mod render;
pub mod resolve;
pub mod rustdoc_json;
pub mod search;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::{Context, Result};
use rustdoc_types::{ItemEnum, Visibility};
use cli::BriefArgs;
use model::{CrateModel, compute_reachable_set};
struct GlobExpansionResult {
item_names: HashMap<String, Vec<String>>,
source_models: HashMap<String, CrateModel>,
}
pub fn run_pipeline(args: &BriefArgs) -> Result<String> {
if let Some(type_name) = &args.methods_of {
let mut args = args.clone();
args.search = Some(type_name.clone());
args.no_structs = true;
args.no_enums = true;
args.no_traits = true;
args.no_unions = true;
args.no_constants = true;
args.no_macros = true;
args.no_aliases = true;
return run_pipeline(&args);
}
if let Some(spec) = &args.crates {
return run_remote_pipeline(args, spec);
}
let metadata = resolve::load_cargo_metadata(args.manifest_path.as_deref())
.context("Failed to load cargo metadata")?;
let resolved =
resolve::resolve_target(&args.crate_name, args.module_path.as_deref(), &metadata)
.context("Failed to resolve target")?;
let json_path = rustdoc_json::generate_rustdoc_json(
&resolved.package_name,
&args.toolchain,
args.manifest_path.as_deref(),
true, &metadata.target_dir,
)
.with_context(|| {
format!(
"Failed to generate rustdoc JSON for crate '{}'",
resolved.package_name
)
})?;
let krate = rustdoc_json::parse_rustdoc_json(&json_path)
.with_context(|| format!("Failed to parse rustdoc JSON at '{}'", json_path.display()))?;
let model = CrateModel::from_crate(krate);
let observer_crate = args
.at_package
.as_deref()
.or(metadata.current_package.as_deref());
let same_crate = match observer_crate {
Some(obs) => obs == resolved.package_name || obs.replace('-', "_") == model.crate_name(),
None => false,
};
let reachable = if !same_crate {
Some(compute_reachable_set(&model))
} else {
None
};
if let Some(pattern) = &args.search {
let output = search::render_search(
&model,
pattern,
args,
args.at_mod.as_deref(),
same_crate,
reachable.as_ref(),
);
return Ok(output);
}
let mut output = render::render_module_api(
&model,
resolved.module_path.as_deref(),
args,
args.at_mod.as_deref(),
same_crate,
reachable.as_ref(),
);
let result = expand_glob_reexports(
&model,
resolved.module_path.as_deref(),
&args.toolchain,
args.manifest_path.as_deref(),
&metadata.target_dir,
);
apply_glob_expansions(&mut output, &result, args);
Ok(output)
}
fn run_remote_pipeline(args: &BriefArgs, spec: &str) -> Result<String> {
let (name, _) = remote::parse_crate_spec(spec);
let workspace = remote::resolve_workspace(spec, args.features.as_deref(), args.no_cache)
.with_context(|| format!("Failed to create workspace for '{name}'"))?;
let manifest_path = workspace
.path()
.join("Cargo.toml")
.to_string_lossy()
.into_owned();
let metadata = resolve::load_cargo_metadata(Some(&manifest_path))
.context("Failed to load cargo metadata for remote crate")?;
let json_path = rustdoc_json::generate_rustdoc_json(
&name,
&args.toolchain,
Some(&manifest_path),
true, &metadata.target_dir,
)
.with_context(|| format!("Failed to generate rustdoc JSON for remote crate '{name}'"))?;
let krate = rustdoc_json::parse_rustdoc_json(&json_path)?;
let model = CrateModel::from_crate(krate);
let reachable = Some(compute_reachable_set(&model));
if let Some(pattern) = &args.search {
let output = search::render_search(&model, pattern, args, None, false, reachable.as_ref());
return Ok(output);
}
let mut output = render::render_module_api(
&model,
args.module_path.as_deref(),
args,
None,
false,
reachable.as_ref(),
);
let result = expand_glob_reexports(
&model,
args.module_path.as_deref(),
&args.toolchain,
Some(&manifest_path),
&metadata.target_dir,
);
apply_glob_expansions(&mut output, &result, args);
Ok(output)
}
fn apply_glob_expansions(output: &mut String, result: &GlobExpansionResult, args: &BriefArgs) {
if args.expand_glob && !result.source_models.is_empty() {
let mut seen_names = HashSet::new();
for (source, source_model) in &result.source_models {
let rendered = render::render_inlined_items(source_model, args, &mut seen_names);
let pattern = format!("pub use {source}::*;");
replace_glob_lines(output, &pattern, &rendered);
}
} else if !result.item_names.is_empty() {
for (source, items) in &result.item_names {
let pattern = format!("pub use {source}::*;");
let mut replacement = String::new();
for name in items {
replacement.push_str(&format!("pub use {source}::{name};\n"));
}
replace_glob_lines(output, &pattern, &replacement);
}
}
}
fn replace_glob_lines(output: &mut String, pattern: &str, replacement: &str) {
loop {
let Some((start, end, indent)) = find_normalized_line(output, pattern) else {
break;
};
let indented: String = replacement
.lines()
.map(|l| {
if l.is_empty() {
"\n".to_string()
} else {
format!("{indent}{l}\n")
}
})
.collect();
output.replace_range(start..end, &indented);
}
}
fn find_normalized_line(text: &str, pattern: &str) -> Option<(usize, usize, String)> {
let mut start = 0;
for line in text.split('\n') {
let end = start + line.len() + 1; let normalized: String = line.split_whitespace().collect::<Vec<_>>().join(" ");
if normalized == pattern {
let indent = &line[..line.len() - line.trim_start().len()];
return Some((start, end.min(text.len()), indent.to_string()));
}
start = end;
}
None
}
fn expand_glob_reexports(
model: &CrateModel,
target_module_path: Option<&str>,
toolchain: &str,
manifest_path: Option<&str>,
target_dir: &Path,
) -> GlobExpansionResult {
let target_item = if let Some(path) = target_module_path {
model.find_module(path)
} else {
model.root_module()
};
let Some(target_item) = target_item else {
return GlobExpansionResult {
item_names: HashMap::new(),
source_models: HashMap::new(),
};
};
let mut item_names = HashMap::new();
let mut source_models = HashMap::new();
for (_id, child) in model.module_children(target_item) {
let ItemEnum::Use(use_item) = &child.inner else {
continue;
};
if !use_item.is_glob {
continue;
}
let source = &use_item.source;
let Ok(json_path) = rustdoc_json::generate_rustdoc_json(
source,
toolchain,
manifest_path,
false,
target_dir,
) else {
continue;
};
let Ok(source_krate) = rustdoc_json::parse_rustdoc_json(&json_path) else {
continue;
};
let source_model = CrateModel::from_crate(source_krate);
let Some(root) = source_model.root_module() else {
continue;
};
let mut items: Vec<String> = source_model
.module_children(root)
.iter()
.filter(|(_, item)| matches!(item.visibility, Visibility::Public))
.filter(|(_, item)| !matches!(item.inner, ItemEnum::Module(_)))
.filter_map(|(_, item)| {
item.name.clone().or_else(|| {
if let ItemEnum::Use(u) = &item.inner {
Some(u.name.clone())
} else {
None
}
})
})
.collect();
items.sort();
items.dedup();
item_names.insert(source.clone(), items);
source_models.insert(source.clone(), source_model);
}
GlobExpansionResult {
item_names,
source_models,
}
}