1pub mod cfg_parse;
2pub mod cli;
3pub mod code;
4pub mod cross_crate;
5pub mod examples;
6pub mod features;
7pub mod lsp;
8pub mod model;
9pub mod remote;
10pub mod render;
11pub mod resolve;
12pub mod rustdoc_json;
13pub mod search;
14pub mod summary;
15pub mod ts;
16
17pub fn clean_cache(spec: &str) -> anyhow::Result<()> {
19 remote::clean_cache(spec)
20}
21
22pub fn run_features_pipeline(
24 args: &cli::FeaturesArgs,
25 remote: &cli::RemoteOpts,
26) -> anyhow::Result<String> {
27 use anyhow::Context;
28
29 if remote.crates {
30 let spec = &args.crate_name;
31 match remote::load_remote_feature_graph(spec)? {
32 Some(graph) => return Ok(features::render_features(&graph)),
33 None => {
34 eprintln!(
35 "warning: feature graph unavailable for '{spec}'; \
36 crates.io unreachable and no cached payload found"
37 );
38 anyhow::bail!("Cannot show features for '{spec}': no data available offline");
39 }
40 }
41 }
42
43 let metadata = resolve::load_cargo_metadata(args.manifest_path.as_deref())
44 .context("Failed to load cargo metadata")?;
45
46 let target = if args.crate_name == "self" {
47 metadata
48 .current_package
49 .as_deref()
50 .context("Cannot resolve 'self': no package found for current directory")?
51 .to_string()
52 } else {
53 args.crate_name.clone()
54 };
55
56 let graph = metadata
57 .feature_graphs
58 .get(&target)
59 .with_context(|| format!("No feature data found for crate '{target}'"))?;
60
61 Ok(features::render_features(graph))
62}
63
64pub fn run_lsp_command(args: &cli::LspArgs, remote: &cli::RemoteOpts) -> anyhow::Result<()> {
66 lsp::run_lsp_command(args, remote)
67}
68
69use rustdoc_json::LockfilePackages;
70use std::collections::{HashMap, HashSet};
71use std::path::{Path, PathBuf};
72
73use anyhow::{Context, Result, bail};
74use rustdoc_types::{ItemEnum, Visibility};
75
76use cli::{
77 ApiArgs, CodeArgs, ExamplesArgs, FilterArgs, RemoteOpts, SearchArgs, SummaryArgs, TsArgs,
78};
79use model::{CrateModel, ReachableInfo, compute_reachable_set};
80
81struct GlobExpansionResult {
84 item_names: HashMap<String, Vec<String>>,
86 source_models: HashMap<String, Vec<CrateModel>>,
89 named_reexports: HashMap<String, Vec<(String, String)>>,
91}
92
93struct PipelineContext {
95 manifest_path: Option<String>,
96 target_dir: PathBuf,
97 package_name: String,
98 module_path: Option<String>,
99 observer_package: Option<String>,
101 toolchain: String,
102 verbose: bool,
103 use_cache: bool,
105 workspace_members: HashSet<String>,
108 available_packages: LockfilePackages,
110 crate_header: Option<String>,
112 _workspace: Option<remote::WorkspaceDir>,
114}
115
116fn generate_and_parse_model(
118 ctx: &PipelineContext,
119) -> Result<(CrateModel, bool, Option<ReachableInfo>)> {
120 if ctx.verbose {
121 eprintln!(
122 "[cargo-brief] Running cargo rustdoc for '{}'...",
123 ctx.package_name
124 );
125 }
126 let json_path = rustdoc_json::generate_rustdoc_json(
127 &ctx.package_name,
128 &ctx.toolchain,
129 ctx.manifest_path.as_deref(),
130 true, &ctx.target_dir,
132 ctx.verbose,
133 ctx.use_cache,
134 )
135 .with_context(|| format!("Failed to generate rustdoc JSON for '{}'", ctx.package_name))?;
136
137 if ctx.verbose {
138 eprintln!("[cargo-brief] Parsing rustdoc JSON...");
139 }
140 let krate = rustdoc_json::parse_rustdoc_json_cached(&json_path)
141 .with_context(|| format!("Failed to parse rustdoc JSON at '{}'", json_path.display()))?;
142 let model = CrateModel::from_crate(krate);
143
144 let same_crate = match &ctx.observer_package {
145 Some(obs) => obs == &ctx.package_name || obs.replace('-', "_") == model.crate_name(),
146 None => false,
147 };
148 let reachable = if !same_crate {
149 Some(compute_reachable_set(&model))
150 } else {
151 None
152 };
153
154 Ok((model, same_crate, reachable))
155}
156
157pub fn run_api_pipeline(args: &ApiArgs, remote: &RemoteOpts) -> Result<String> {
159 let ctx = if remote.crates {
160 let spec = &args.target.crate_name;
161 build_remote_context_api(args, spec, remote)?
162 } else {
163 build_local_context_api(args)?
164 };
165 run_shared_api_pipeline(&ctx, args)
166}
167
168fn build_local_context_api(args: &ApiArgs) -> Result<PipelineContext> {
169 if args.global.verbose {
170 eprintln!(
171 "[cargo-brief] Resolving target '{}'...",
172 args.target.crate_name
173 );
174 }
175 let metadata = resolve::load_cargo_metadata(args.target.manifest_path.as_deref())
176 .context("Failed to load cargo metadata")?;
177
178 let resolved = resolve::resolve_target(
179 &args.target.crate_name,
180 args.target.module_path.as_deref(),
181 &metadata,
182 )
183 .context("Failed to resolve target")?;
184
185 let observer_package = args
186 .target
187 .at_package
188 .clone()
189 .or(metadata.current_package.clone());
190
191 let available_packages =
192 rustdoc_json::load_lockfile_packages(args.target.manifest_path.as_deref());
193
194 let is_workspace_member = metadata.workspace_packages.contains(&resolved.package_name);
195
196 Ok(PipelineContext {
197 manifest_path: args.target.manifest_path.clone(),
198 target_dir: metadata.target_dir,
199 package_name: resolved.package_name,
200 module_path: resolved.module_path,
201 observer_package,
202 toolchain: args.global.toolchain.clone(),
203 verbose: args.global.verbose,
204 use_cache: !is_workspace_member,
205 workspace_members: metadata.workspace_packages.into_iter().collect(),
206 available_packages,
207 crate_header: None,
208 _workspace: None,
209 })
210}
211
212fn build_remote_context_api(
213 args: &ApiArgs,
214 spec: &str,
215 remote: &RemoteOpts,
216) -> Result<PipelineContext> {
217 let (actual_spec, module_path) = if let Some(idx) = spec.find("::") {
221 let rest = &spec[idx + 2..];
222 let module = if rest.is_empty() {
223 None
224 } else {
225 Some(rest.to_string())
226 };
227 (&spec[..idx], module)
228 } else {
229 (spec, args.target.module_path.clone())
230 };
231
232 let (name, _) = remote::parse_crate_spec(actual_spec);
233 if args.global.verbose {
234 eprintln!("[cargo-brief] Resolving workspace for '{name}'...");
235 }
236 let (workspace, resolved_version) = remote::resolve_workspace(
237 actual_spec,
238 remote.features.as_deref(),
239 remote.no_default_features,
240 remote.no_cache,
241 )
242 .with_context(|| format!("Failed to create workspace for '{name}'"))?;
243
244 let manifest_path = workspace
245 .path()
246 .join("Cargo.toml")
247 .to_string_lossy()
248 .into_owned();
249
250 if let Some(requested) = remote.features.as_deref() {
252 match remote::load_remote_feature_graph(actual_spec) {
253 Ok(Some(graph)) => features::validate_requested_features(&graph, requested)?,
254 Ok(None) => eprintln!(
255 "warning: feature graph unavailable for '{name}'; \
256 -F values will not be validated and feature gates will not be annotated"
257 ),
258 Err(_) => {} }
260 }
261
262 let metadata = resolve::load_cargo_metadata(Some(&manifest_path))
263 .context("Failed to load cargo metadata for remote crate")?;
264
265 let crate_header = build_remote_crate_header(
266 &name,
267 resolved_version.as_deref(),
268 workspace.path(),
269 remote.features.as_deref(),
270 );
271
272 let available_packages = rustdoc_json::load_lockfile_packages(Some(&manifest_path));
273
274 Ok(PipelineContext {
275 manifest_path: Some(manifest_path),
276 target_dir: metadata.target_dir,
277 package_name: name,
278 module_path,
279 observer_package: None, toolchain: args.global.toolchain.clone(),
281 verbose: args.global.verbose,
282 use_cache: true, workspace_members: HashSet::new(), available_packages,
285 crate_header,
286 _workspace: Some(workspace),
287 })
288}
289
290fn run_shared_api_pipeline(ctx: &PipelineContext, args: &ApiArgs) -> Result<String> {
291 let (model, same_crate, reachable) = generate_and_parse_model(ctx)?;
292 let has_cross_crate = cross_crate::root_has_cross_crate_reexports(&model);
293 if has_cross_crate {
294 pre_warm_cross_crate_json(&model, ctx);
295 }
296
297 let mut output = if let Some(ref module_path) = ctx.module_path {
298 if model.find_module(module_path).is_some() {
300 render_and_expand_globs(
301 &model,
302 Some(module_path),
303 args,
304 ctx,
305 same_crate,
306 reachable.as_ref(),
307 )?
308 } else {
309 if ctx.verbose {
311 eprintln!(
312 "[cargo-brief] Module '{module_path}' not found locally, trying cross-crate resolution..."
313 );
314 }
315 if let Some(resolution) = cross_crate::resolve_cross_crate_module(
316 &model,
317 module_path,
318 &ctx.toolchain,
319 ctx.manifest_path.as_deref(),
320 &ctx.target_dir,
321 ctx.verbose,
322 ) {
323 let sub_reachable = Some(compute_reachable_set(&resolution.model));
324 let mut output = render::render_module_api(
325 &resolution.model,
326 resolution.inner_module_path.as_deref(),
327 args,
328 None,
329 false,
330 sub_reachable.as_ref(),
331 );
332 let result = expand_glob_reexports(
333 &resolution.model,
334 resolution.inner_module_path.as_deref(),
335 &ctx.toolchain,
336 ctx.manifest_path.as_deref(),
337 &ctx.target_dir,
338 ctx.verbose,
339 &ctx.workspace_members,
340 );
341 apply_glob_expansions(&mut output, &result, !args.no_expand_glob, &args.filter);
342 output
343 } else {
344 let leaf_result = if let Some((parent, leaf_name)) = module_path.rsplit_once("::") {
346 model.find_item_in_module(parent, leaf_name)
347 } else {
348 model.find_item_in_module("", module_path)
349 };
350
351 if let Some((item_id, item)) = leaf_result {
352 render::render_leaf_item(
353 &model,
354 item,
355 item_id,
356 args,
357 if same_crate {
358 args.target.at_mod.as_deref()
359 } else {
360 None
361 },
362 same_crate,
363 reachable.as_ref(),
364 )
365 } else {
366 let (parent_path, leaf_name) =
368 if let Some((p, l)) = module_path.rsplit_once("::") {
369 (p, l)
370 } else {
371 ("", module_path.as_str())
372 };
373
374 let parent_exists = if parent_path.is_empty() {
375 model.root_module().is_some()
376 } else {
377 model.find_module(parent_path).is_some()
378 };
379
380 if parent_exists {
381 render::render_leaf_not_found(
382 &model,
383 parent_path,
384 leaf_name,
385 same_crate,
386 reachable.as_ref(),
387 )
388 } else {
389 render_and_expand_globs(
391 &model,
392 Some(module_path),
393 args,
394 ctx,
395 same_crate,
396 reachable.as_ref(),
397 )?
398 }
399 }
400 }
401 }
402 } else if args.recursive && has_cross_crate {
403 let mut output =
405 render_and_expand_globs(&model, None, args, ctx, same_crate, reachable.as_ref())?;
406 if ctx.verbose {
407 eprintln!("[cargo-brief] Building cross-crate accessible path index...");
408 }
409 let index = cross_crate::build_cross_crate_index(
410 &model,
411 &ctx.toolchain,
412 ctx.manifest_path.as_deref(),
413 &ctx.target_dir,
414 ctx.verbose,
415 &ctx.workspace_members,
416 &ctx.available_packages,
417 );
418 let cross_output = render::render_cross_crate_api(&index, model.crate_name(), args);
419 if !cross_output.is_empty() {
420 output.push_str(&cross_output);
421 }
422 output
423 } else {
424 render_and_expand_globs(
426 &model,
427 ctx.module_path.as_deref(),
428 args,
429 ctx,
430 same_crate,
431 reachable.as_ref(),
432 )?
433 };
434
435 if let Some(header) = &ctx.crate_header
437 && let Some(first_newline) = output.find('\n')
438 {
439 let first_line = &output[..first_newline];
440 if first_line.starts_with("// crate ") {
441 output.replace_range(..first_newline, header);
442 }
443 }
444
445 Ok(output)
446}
447
448fn render_and_expand_globs(
450 model: &CrateModel,
451 module_path: Option<&str>,
452 args: &ApiArgs,
453 ctx: &PipelineContext,
454 same_crate: bool,
455 reachable: Option<&ReachableInfo>,
456) -> Result<String> {
457 let mut output = render::render_module_api(
458 model,
459 module_path,
460 args,
461 if same_crate {
462 args.target.at_mod.as_deref()
463 } else {
464 None
465 },
466 same_crate,
467 reachable,
468 );
469 let result = expand_glob_reexports(
470 model,
471 module_path,
472 &ctx.toolchain,
473 ctx.manifest_path.as_deref(),
474 &ctx.target_dir,
475 ctx.verbose,
476 &ctx.workspace_members,
477 );
478 apply_glob_expansions(&mut output, &result, !args.no_expand_glob, &args.filter);
479 Ok(output)
480}
481
482pub fn run_search_pipeline(args: &SearchArgs, remote: &RemoteOpts) -> Result<String> {
484 if args.patterns.is_empty()
486 && args.methods_of.is_none()
487 && args.in_params.is_none()
488 && args.in_returns.is_none()
489 {
490 anyhow::bail!("search requires a pattern, --methods-of, --in-params, or --in-returns");
491 }
492
493 let mut args = args.clone();
494
495 if let Some(methods_of) = &args.methods_of {
498 if args.patterns.is_empty() {
499 args.patterns = vec![methods_of.clone()];
500 }
501 apply_function_narrowing(&mut args.filter);
502 }
503
504 if args.in_params.is_some() || args.in_returns.is_some() {
506 apply_function_narrowing(&mut args.filter);
507 }
508
509 let ctx = if remote.crates {
510 build_remote_context_search(&args, &args.crate_name, remote)?
511 } else {
512 build_local_context_search(&args)?
513 };
514 run_shared_search_pipeline(&ctx, &args)
515}
516
517fn apply_function_narrowing(filter: &mut FilterArgs) {
519 filter.no_structs = true;
520 filter.no_enums = true;
521 filter.no_traits = true;
522 filter.no_unions = true;
523 filter.no_constants = true;
524 filter.no_macros = true;
525 filter.no_aliases = true;
526}
527
528fn build_local_context_search(args: &SearchArgs) -> Result<PipelineContext> {
529 if args.global.verbose {
530 eprintln!("[cargo-brief] Resolving target '{}'...", args.crate_name);
531 }
532 let metadata = resolve::load_cargo_metadata(args.manifest_path.as_deref())
533 .context("Failed to load cargo metadata")?;
534
535 let resolved = resolve::resolve_target(&args.crate_name, None, &metadata)
536 .context("Failed to resolve target")?;
537
538 let observer_package = args.at_package.clone().or(metadata.current_package.clone());
539
540 let available_packages = rustdoc_json::load_lockfile_packages(args.manifest_path.as_deref());
541 let is_workspace_member = metadata.workspace_packages.contains(&resolved.package_name);
542
543 Ok(PipelineContext {
544 manifest_path: args.manifest_path.clone(),
545 target_dir: metadata.target_dir,
546 package_name: resolved.package_name,
547 module_path: None, observer_package,
549 toolchain: args.global.toolchain.clone(),
550 verbose: args.global.verbose,
551 use_cache: !is_workspace_member,
552 workspace_members: metadata.workspace_packages.into_iter().collect(),
553 available_packages,
554 crate_header: None,
555 _workspace: None,
556 })
557}
558
559fn build_remote_context_search(
560 args: &SearchArgs,
561 spec: &str,
562 remote: &RemoteOpts,
563) -> Result<PipelineContext> {
564 let (name, _) = remote::parse_crate_spec(spec);
565 if args.global.verbose {
566 eprintln!("[cargo-brief] Resolving workspace for '{name}'...");
567 }
568 let (workspace, _resolved_version) = remote::resolve_workspace(
569 spec,
570 remote.features.as_deref(),
571 remote.no_default_features,
572 remote.no_cache,
573 )
574 .with_context(|| format!("Failed to create workspace for '{name}'"))?;
575
576 if let Some(requested) = remote.features.as_deref() {
578 match remote::load_remote_feature_graph(spec) {
579 Ok(Some(graph)) => features::validate_requested_features(&graph, requested)?,
580 Ok(None) => eprintln!(
581 "warning: feature graph unavailable for '{name}'; \
582 -F values will not be validated and feature gates will not be annotated"
583 ),
584 Err(_) => {}
585 }
586 }
587
588 let manifest_path = workspace
589 .path()
590 .join("Cargo.toml")
591 .to_string_lossy()
592 .into_owned();
593
594 let metadata = resolve::load_cargo_metadata(Some(&manifest_path))
595 .context("Failed to load cargo metadata for remote crate")?;
596
597 let available_packages = rustdoc_json::load_lockfile_packages(Some(&manifest_path));
598
599 Ok(PipelineContext {
600 manifest_path: Some(manifest_path),
601 target_dir: metadata.target_dir,
602 package_name: name,
603 module_path: None, observer_package: None, toolchain: args.global.toolchain.clone(),
606 verbose: args.global.verbose,
607 use_cache: true, workspace_members: HashSet::new(),
609 available_packages,
610 crate_header: None,
611 _workspace: Some(workspace),
612 })
613}
614
615fn run_shared_search_pipeline(ctx: &PipelineContext, args: &SearchArgs) -> Result<String> {
616 let (model, same_crate, reachable) = generate_and_parse_model(ctx)?;
617 let pattern = args.pattern();
618 let methods_of = args.methods_of.as_deref();
619 let in_params = args.in_params.as_deref();
620 let in_returns = args.in_returns.as_deref();
621
622 let search_kind = args.search_kind.as_deref();
623 let members = args.members;
624 let search_fn = |model: &CrateModel,
625 observer: Option<&str>,
626 same_crate: bool,
627 reachable: Option<&ReachableInfo>,
628 header_total: Option<usize>| {
629 search::render_search_filtered_counted(
630 model,
631 &pattern,
632 &args.filter,
633 args.limit.as_deref(),
634 observer,
635 same_crate,
636 reachable,
637 methods_of,
638 search_kind,
639 members,
640 in_params,
641 in_returns,
642 header_total,
643 )
644 };
645
646 let local_output = search_fn(
647 &model,
648 if same_crate {
649 args.at_mod.as_deref()
650 } else {
651 None
652 },
653 same_crate,
654 reachable.as_ref(),
655 None,
656 );
657 let mut output = local_output.output;
658
659 if cross_crate::root_has_cross_crate_reexports(&model) {
661 pre_warm_cross_crate_json(&model, ctx);
662 if ctx.verbose {
663 eprintln!("[cargo-brief] Building cross-crate accessible path index...");
664 }
665 let index = cross_crate::build_cross_crate_index(
666 &model,
667 &ctx.toolchain,
668 ctx.manifest_path.as_deref(),
669 &ctx.target_dir,
670 ctx.verbose,
671 &ctx.workspace_members,
672 &ctx.available_packages,
673 );
674 let cross_output = search::search_cross_crate_index_counted(
675 &index,
676 model.crate_name(),
677 &pattern,
678 &args.filter,
679 args.limit.as_deref(),
680 search_kind,
681 methods_of,
682 members,
683 in_params,
684 in_returns,
685 );
686 if cross_output.total > 0 {
687 output = search_fn(
688 &model,
689 if same_crate {
690 args.at_mod.as_deref()
691 } else {
692 None
693 },
694 same_crate,
695 reachable.as_ref(),
696 Some(local_output.total + cross_output.total),
697 )
698 .output;
699 }
700 if !cross_output.output.is_empty() {
701 output.push_str(&cross_output.output);
702 }
703 }
704
705 Ok(output)
706}
707
708pub fn run_examples_pipeline(args: &ExamplesArgs, remote: &RemoteOpts) -> Result<String> {
710 if remote.crates {
711 let spec = &args.crate_name;
713 let (name, _) = remote::parse_crate_spec(spec);
714 if args.global.verbose {
715 eprintln!("[cargo-brief] Resolving workspace for '{name}'...");
716 }
717 let (workspace, resolved_version) = remote::resolve_workspace(
718 spec,
719 remote.features.as_deref(),
720 remote.no_default_features,
721 remote.no_cache,
722 )
723 .with_context(|| format!("Failed to create workspace for '{name}'"))?;
724
725 let manifest_path = workspace
726 .path()
727 .join("Cargo.toml")
728 .to_string_lossy()
729 .into_owned();
730
731 if args.global.verbose {
732 eprintln!("[cargo-brief] Finding source root for '{name}'...");
733 }
734 let source_root = resolve::find_dep_source_root(&manifest_path, &name)
735 .with_context(|| format!("Failed to find source root for '{name}'"))?;
736
737 let version =
738 resolved_version.or_else(|| remote::resolve_crate_version(workspace.path(), &name));
739 let crate_display = match version {
740 Some(v) => format!("{name}[{v}]"),
741 None => name.clone(),
742 };
743
744 Ok(examples::render_examples(
745 &source_root,
746 &crate_display,
747 args,
748 ))
749 } else {
750 let metadata = resolve::load_cargo_metadata(args.manifest_path.as_deref())
752 .context("Failed to load cargo metadata")?;
753
754 let (pkg_name, source_root) = if args.crate_name == "self" {
755 let pkg = metadata.current_package.as_ref().ok_or_else(|| {
756 anyhow::anyhow!(
757 "Cannot resolve 'self': no package found for the current directory."
758 )
759 })?;
760 let dir = metadata
761 .package_manifest_dirs
762 .get(pkg)
763 .cloned()
764 .or(metadata.current_package_manifest_dir.clone())
765 .ok_or_else(|| {
766 anyhow::anyhow!("Cannot find manifest directory for package '{pkg}'")
767 })?;
768 (pkg.clone(), dir)
769 } else {
770 let normalized = args.crate_name.replace('-', "_");
772 let found = metadata
773 .package_manifest_dirs
774 .iter()
775 .find(|(k, _)| k.replace('-', "_") == normalized);
776 match found {
777 Some((name, dir)) => (name.clone(), dir.clone()),
778 None => {
779 anyhow::bail!(
780 "Package '{}' not found in workspace. Available: {}",
781 args.crate_name,
782 metadata.workspace_packages.join(", ")
783 );
784 }
785 }
786 };
787
788 if args.global.verbose {
789 eprintln!("[cargo-brief] Scanning examples for '{pkg_name}'...");
790 }
791
792 Ok(examples::render_examples(&source_root, &pkg_name, args))
793 }
794}
795
796pub fn run_ts_pipeline(args: &TsArgs, remote: &RemoteOpts) -> Result<String> {
798 if remote.crates {
799 let spec = &args.crate_name;
800 let (name, _) = remote::parse_crate_spec(spec);
801 if args.global.verbose {
802 eprintln!("[cargo-brief] Resolving workspace for '{name}'...");
803 }
804 let (workspace, _resolved_version) = remote::resolve_workspace(
805 spec,
806 remote.features.as_deref(),
807 remote.no_default_features,
808 remote.no_cache,
809 )
810 .with_context(|| format!("Failed to create workspace for '{name}'"))?;
811
812 let manifest_path = workspace
813 .path()
814 .join("Cargo.toml")
815 .to_string_lossy()
816 .into_owned();
817
818 if args.global.verbose {
819 eprintln!("[cargo-brief] Finding source root for '{name}'...");
820 }
821 let source_root = resolve::find_dep_source_root(&manifest_path, &name)
822 .with_context(|| format!("Failed to find source root for '{name}'"))?;
823
824 if args.global.verbose {
825 eprintln!("[cargo-brief] Running tree-sitter query on '{name}'...");
826 }
827
828 ts::run_query(&source_root, &args.query, args)
829 } else {
830 let metadata = resolve::load_cargo_metadata(args.manifest_path.as_deref())
831 .context("Failed to load cargo metadata")?;
832
833 let (_pkg_name, source_root) = if args.crate_name == "self" {
834 let pkg = metadata.current_package.as_ref().ok_or_else(|| {
835 anyhow::anyhow!(
836 "Cannot resolve 'self': no package found for the current directory."
837 )
838 })?;
839 let dir = metadata
840 .package_manifest_dirs
841 .get(pkg)
842 .cloned()
843 .or(metadata.current_package_manifest_dir.clone())
844 .ok_or_else(|| {
845 anyhow::anyhow!("Cannot find manifest directory for package '{pkg}'")
846 })?;
847 (pkg.clone(), dir)
848 } else {
849 let normalized = args.crate_name.replace('-', "_");
850 let found = metadata
851 .package_manifest_dirs
852 .iter()
853 .find(|(k, _)| k.replace('-', "_") == normalized);
854 match found {
855 Some((name, dir)) => (name.clone(), dir.clone()),
856 None => {
857 anyhow::bail!(
858 "Package '{}' not found in workspace. Available: {}",
859 args.crate_name,
860 metadata.workspace_packages.join(", ")
861 );
862 }
863 }
864 };
865
866 if args.global.verbose {
867 eprintln!(
868 "[cargo-brief] Running tree-sitter query on '{}'...",
869 args.crate_name
870 );
871 }
872
873 ts::run_query(&source_root, &args.query, args)
874 }
875}
876
877pub fn run_code_pipeline(args: &CodeArgs, remote: &RemoteOpts) -> Result<String> {
879 let resolved = code::resolve_code_args(args)?;
880
881 struct CodeTarget {
883 primary_sources: Vec<(String, PathBuf)>,
885 effective_manifest: String,
886 target_dir: PathBuf,
887 is_workspace_member: bool,
888 dep_root_pkg: String,
890 _workspace: Option<remote::WorkspaceDir>,
891 }
892
893 let target = if remote.crates {
894 if resolved.target == "self" {
896 bail!("-C (remote) mode requires an explicit crate spec as TARGET");
897 }
898 let spec = &resolved.target;
899 let (crate_name, _) = remote::parse_crate_spec(spec);
900 if args.global.verbose {
901 eprintln!("[cargo-brief] Resolving workspace for '{crate_name}'...");
902 }
903 let (workspace, _resolved_version) = remote::resolve_workspace(
904 spec,
905 remote.features.as_deref(),
906 remote.no_default_features,
907 remote.no_cache,
908 )
909 .with_context(|| format!("Failed to create workspace for '{crate_name}'"))?;
910
911 let manifest_path = workspace
912 .path()
913 .join("Cargo.toml")
914 .to_string_lossy()
915 .into_owned();
916
917 if args.global.verbose {
918 eprintln!("[cargo-brief] Finding source root for '{crate_name}'...");
919 }
920 let source_root = resolve::find_dep_source_root(&manifest_path, &crate_name)
921 .with_context(|| format!("Failed to find source root for '{crate_name}'"))?;
922
923 let target_dir = if !args.no_deps {
925 let meta = resolve::load_cargo_metadata(Some(&manifest_path))
926 .context("Failed to load cargo metadata for remote crate")?;
927 meta.target_dir
928 } else {
929 PathBuf::new()
930 };
931
932 CodeTarget {
933 primary_sources: vec![(crate_name.to_string(), source_root)],
934 effective_manifest: manifest_path,
935 target_dir,
936 is_workspace_member: false,
937 dep_root_pkg: crate_name.to_string(),
938 _workspace: Some(workspace),
939 }
940 } else {
941 let metadata = resolve::load_cargo_metadata(args.manifest_path.as_deref())
942 .context("Failed to load cargo metadata")?;
943
944 if resolved.target == "self" {
945 let mut primary_sources = Vec::new();
947 for pkg in &metadata.workspace_packages {
948 if let Some(dir) = metadata.package_manifest_dirs.get(pkg) {
949 primary_sources.push((pkg.clone(), dir.clone()));
950 }
951 }
952 if primary_sources.is_empty() {
953 bail!("No workspace packages found. Run from inside a Cargo project.");
954 }
955
956 let effective_manifest = args
957 .manifest_path
958 .clone()
959 .unwrap_or_else(|| "Cargo.toml".to_string());
960
961 let dep_root_pkg = metadata
962 .current_package
963 .clone()
964 .or_else(|| metadata.workspace_packages.first().cloned())
965 .unwrap_or_default();
966
967 CodeTarget {
968 primary_sources,
969 effective_manifest,
970 target_dir: metadata.target_dir,
971 is_workspace_member: true,
972 dep_root_pkg,
973 _workspace: None,
974 }
975 } else {
976 let normalized = resolved.target.replace('-', "_");
978 let found = metadata
979 .package_manifest_dirs
980 .iter()
981 .find(|(k, _)| k.replace('-', "_") == normalized);
982 let (pkg_name, source_root) = match found {
983 Some((name, dir)) => (name.clone(), dir.clone()),
984 None => {
985 anyhow::bail!(
986 "Package '{}' not found in workspace. Available: {}",
987 resolved.target,
988 metadata.workspace_packages.join(", ")
989 );
990 }
991 };
992
993 let effective_manifest = metadata
994 .package_manifest_dirs
995 .get(&pkg_name)
996 .map(|d| d.join("Cargo.toml").to_string_lossy().into_owned())
997 .or_else(|| args.manifest_path.clone())
998 .unwrap_or_else(|| "Cargo.toml".to_string());
999
1000 CodeTarget {
1001 primary_sources: vec![(pkg_name.clone(), source_root)],
1002 effective_manifest,
1003 target_dir: metadata.target_dir,
1004 is_workspace_member: true,
1005 dep_root_pkg: pkg_name,
1006 _workspace: None,
1007 }
1008 }
1009 };
1010
1011 if args.global.verbose {
1012 let names: Vec<&str> = target
1013 .primary_sources
1014 .iter()
1015 .map(|(n, _)| n.as_str())
1016 .collect();
1017 eprintln!(
1018 "[cargo-brief] Searching {} for code definitions...",
1019 names.join(", ")
1020 );
1021 }
1022
1023 let mut sources = target.primary_sources.clone();
1025
1026 if !args.no_deps {
1027 let dep_sources = if args.all_deps {
1028 collect_all_deps_sources(&target.effective_manifest, &target.dep_root_pkg)?
1029 } else {
1030 collect_accessible_deps_sources(
1031 &target.dep_root_pkg,
1032 &target.effective_manifest,
1033 &target.target_dir,
1034 &args.global.toolchain,
1035 args.global.verbose,
1036 !target.is_workspace_member,
1037 )?
1038 };
1039 let primary_names: std::collections::HashSet<&str> = target
1041 .primary_sources
1042 .iter()
1043 .map(|(n, _)| n.as_str())
1044 .collect();
1045 sources.extend(
1046 dep_sources
1047 .into_iter()
1048 .filter(|(n, _)| !primary_names.contains(n.as_str())),
1049 );
1050 if args.global.verbose {
1051 eprintln!("[cargo-brief] Searching {} crate(s)...", sources.len());
1052 }
1053 }
1054
1055 let mut output = String::new();
1057
1058 if !args.refs_only {
1060 output = code::search_code(
1061 &sources,
1062 &resolved.name,
1063 resolved.kind,
1064 args,
1065 args.in_type.as_deref(),
1066 )?;
1067 }
1068
1069 if args.refs || args.refs_only {
1071 let ref_limit = if args.refs_only {
1072 args.limit.as_deref()
1073 } else {
1074 None
1075 };
1076 let refs = code::search_references(
1077 &sources,
1078 &resolved.name,
1079 args.src_only,
1080 args.quiet,
1081 ref_limit,
1082 );
1083 if !refs.is_empty() && !refs.starts_with("// no references") {
1084 if !output.is_empty() {
1085 output.push_str("\n// --- References ---\n\n");
1086 }
1087 output.push_str(&refs);
1088 } else if args.refs_only {
1089 output = refs;
1090 }
1091 }
1092
1093 Ok(output)
1094}
1095
1096fn collect_all_deps_sources(
1098 manifest_path: &str,
1099 root_package: &str,
1100) -> Result<Vec<(String, PathBuf)>> {
1101 let (all_dirs, direct_deps) =
1102 resolve::load_dep_package_dirs(Some(manifest_path), root_package)?;
1103
1104 let mut result = Vec::new();
1105 for dep_name in &direct_deps {
1106 if let Some(dir) = all_dirs.get(dep_name) {
1107 result.push((dep_name.clone(), dir.clone()));
1108 }
1109 }
1110 Ok(result)
1111}
1112
1113fn collect_accessible_deps_sources(
1115 pkg_name: &str,
1116 manifest_path: &str,
1117 target_dir: &Path,
1118 toolchain: &str,
1119 verbose: bool,
1120 use_cache: bool,
1121) -> Result<Vec<(String, PathBuf)>> {
1122 let json_path = rustdoc_json::generate_rustdoc_json(
1124 pkg_name,
1125 toolchain,
1126 Some(manifest_path),
1127 true, target_dir,
1129 verbose,
1130 use_cache,
1131 )
1132 .with_context(|| format!("Failed to generate rustdoc JSON for '{pkg_name}'"))?;
1133
1134 let krate = rustdoc_json::parse_rustdoc_json_cached(&json_path)
1135 .with_context(|| format!("Failed to parse rustdoc JSON for '{pkg_name}'"))?;
1136
1137 let model = CrateModel::from_crate(krate);
1138
1139 let accessible =
1141 discover_accessible_deps(&model, toolchain, Some(manifest_path), target_dir, verbose);
1142
1143 if accessible.is_empty() {
1144 return Ok(Vec::new());
1145 }
1146
1147 let (all_dirs, _) = resolve::load_dep_package_dirs(Some(manifest_path), pkg_name)?;
1149
1150 let mut result = Vec::new();
1151 for dep_name in &accessible {
1152 let base = dep_name.split('@').next().unwrap_or(dep_name);
1153 if let Some(dir) = all_dirs.get(base) {
1154 result.push((base.to_string(), dir.clone()));
1155 } else {
1156 let alt = base.replace('_', "-");
1158 if let Some(dir) = all_dirs.get(&alt) {
1159 result.push((alt, dir.clone()));
1160 }
1161 }
1162 }
1163 Ok(result)
1164}
1165
1166pub fn run_summary_pipeline(args: &SummaryArgs, remote: &RemoteOpts) -> Result<String> {
1168 let ctx = if remote.crates {
1169 let spec = &args.target.crate_name;
1170 build_remote_context_summary(args, spec, remote)?
1171 } else {
1172 build_local_context_summary(args)?
1173 };
1174 run_shared_summary_pipeline(&ctx)
1175}
1176
1177fn build_local_context_summary(args: &SummaryArgs) -> Result<PipelineContext> {
1178 if args.global.verbose {
1179 eprintln!(
1180 "[cargo-brief] Resolving target '{}'...",
1181 args.target.crate_name
1182 );
1183 }
1184 let metadata = resolve::load_cargo_metadata(args.target.manifest_path.as_deref())
1185 .context("Failed to load cargo metadata")?;
1186
1187 let resolved = resolve::resolve_target(
1188 &args.target.crate_name,
1189 args.target.module_path.as_deref(),
1190 &metadata,
1191 )
1192 .context("Failed to resolve target")?;
1193
1194 let observer_package = args
1195 .target
1196 .at_package
1197 .clone()
1198 .or(metadata.current_package.clone());
1199
1200 let available_packages =
1201 rustdoc_json::load_lockfile_packages(args.target.manifest_path.as_deref());
1202 let is_workspace_member = metadata.workspace_packages.contains(&resolved.package_name);
1203
1204 Ok(PipelineContext {
1205 manifest_path: args.target.manifest_path.clone(),
1206 target_dir: metadata.target_dir,
1207 package_name: resolved.package_name,
1208 module_path: resolved.module_path,
1209 observer_package,
1210 toolchain: args.global.toolchain.clone(),
1211 verbose: args.global.verbose,
1212 use_cache: !is_workspace_member,
1213 workspace_members: metadata.workspace_packages.into_iter().collect(),
1214 available_packages,
1215 crate_header: None,
1216 _workspace: None,
1217 })
1218}
1219
1220fn build_remote_context_summary(
1221 args: &SummaryArgs,
1222 spec: &str,
1223 remote: &RemoteOpts,
1224) -> Result<PipelineContext> {
1225 let (actual_spec, module_path) = if let Some(idx) = spec.find("::") {
1227 let rest = &spec[idx + 2..];
1228 let module = if rest.is_empty() {
1229 None
1230 } else {
1231 Some(rest.to_string())
1232 };
1233 (&spec[..idx], module)
1234 } else {
1235 (spec, args.target.module_path.clone())
1236 };
1237
1238 let (name, _) = remote::parse_crate_spec(actual_spec);
1239 if args.global.verbose {
1240 eprintln!("[cargo-brief] Resolving workspace for '{name}'...");
1241 }
1242 let (workspace, resolved_version) = remote::resolve_workspace(
1243 actual_spec,
1244 remote.features.as_deref(),
1245 remote.no_default_features,
1246 remote.no_cache,
1247 )
1248 .with_context(|| format!("Failed to create workspace for '{name}'"))?;
1249
1250 if let Some(requested) = remote.features.as_deref() {
1252 match remote::load_remote_feature_graph(actual_spec) {
1253 Ok(Some(graph)) => features::validate_requested_features(&graph, requested)?,
1254 Ok(None) => eprintln!(
1255 "warning: feature graph unavailable for '{name}'; \
1256 -F values will not be validated and feature gates will not be annotated"
1257 ),
1258 Err(_) => {}
1259 }
1260 }
1261
1262 let manifest_path = workspace
1263 .path()
1264 .join("Cargo.toml")
1265 .to_string_lossy()
1266 .into_owned();
1267
1268 let metadata = resolve::load_cargo_metadata(Some(&manifest_path))
1269 .context("Failed to load cargo metadata for remote crate")?;
1270
1271 let crate_header = build_remote_crate_header(
1272 &name,
1273 resolved_version.as_deref(),
1274 workspace.path(),
1275 remote.features.as_deref(),
1276 );
1277
1278 let available_packages = rustdoc_json::load_lockfile_packages(Some(&manifest_path));
1279
1280 Ok(PipelineContext {
1281 manifest_path: Some(manifest_path),
1282 target_dir: metadata.target_dir,
1283 package_name: name,
1284 module_path,
1285 observer_package: None,
1286 toolchain: args.global.toolchain.clone(),
1287 verbose: args.global.verbose,
1288 use_cache: true,
1289 workspace_members: HashSet::new(),
1290 available_packages,
1291 crate_header,
1292 _workspace: Some(workspace),
1293 })
1294}
1295
1296fn run_shared_summary_pipeline(ctx: &PipelineContext) -> Result<String> {
1297 let (model, same_crate, reachable) = generate_and_parse_model(ctx)?;
1298
1299 let mut output = summary::render_summary(
1300 &model,
1301 ctx.module_path.as_deref(),
1302 same_crate,
1303 reachable.as_ref(),
1304 );
1305
1306 if ctx.module_path.is_none() && cross_crate::root_has_cross_crate_reexports(&model) {
1308 pre_warm_cross_crate_json(&model, ctx);
1309 if ctx.verbose {
1310 eprintln!("[cargo-brief] Building cross-crate accessible path index...");
1311 }
1312 let index = cross_crate::build_cross_crate_index(
1313 &model,
1314 &ctx.toolchain,
1315 ctx.manifest_path.as_deref(),
1316 &ctx.target_dir,
1317 ctx.verbose,
1318 &ctx.workspace_members,
1319 &ctx.available_packages,
1320 );
1321 let cross_summary = summary::summarize_cross_crate_index(&index);
1322 if !cross_summary.is_empty() {
1323 output.push_str(&cross_summary);
1324 }
1325 }
1326
1327 if let Some(header) = &ctx.crate_header
1329 && let Some(first_newline) = output.find('\n')
1330 {
1331 let first_line = &output[..first_newline];
1332 if first_line.starts_with("// crate ") {
1333 output.replace_range(..first_newline, header);
1334 }
1335 }
1336
1337 Ok(output)
1338}
1339
1340fn pre_warm_cross_crate_json(model: &CrateModel, ctx: &PipelineContext) {
1345 let mut seen = HashSet::new();
1346
1347 let mut batch: Vec<String> = cross_crate::collect_external_crate_names(model)
1350 .into_iter()
1351 .filter_map(|n| normalize_to_lockfile_name(&n, &ctx.available_packages))
1352 .collect();
1353 batch.sort();
1354 batch.dedup();
1355 seen.extend(batch.iter().cloned());
1356
1357 const MAX_DEPTH: usize = 8;
1358 for _ in 0..MAX_DEPTH {
1359 if batch.is_empty() {
1360 break;
1361 }
1362
1363 let refs: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
1364 rustdoc_json::batch_generate_rustdoc_json(
1365 &refs,
1366 &ctx.toolchain,
1367 ctx.manifest_path.as_deref(),
1368 &ctx.target_dir,
1369 ctx.verbose,
1370 );
1371
1372 let mut next_batch = Vec::new();
1374 for name in &batch {
1375 let doc_dir = ctx.target_dir.join("doc");
1376 let Some(json_path) =
1377 rustdoc_json::find_lib_json_path(name, ctx.manifest_path.as_deref(), &doc_dir)
1378 else {
1379 continue;
1380 };
1381 let Ok(krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
1382 continue;
1383 };
1384 let sub_model = CrateModel::from_crate(krate);
1385 for sub_name in cross_crate::collect_external_crate_names(&sub_model) {
1386 if let Some(pkg_name) =
1387 normalize_to_lockfile_name(&sub_name, &ctx.available_packages)
1388 && !seen.contains(&pkg_name)
1389 {
1390 seen.insert(pkg_name.clone());
1391 next_batch.push(pkg_name);
1392 }
1393 }
1394 }
1395 next_batch.sort();
1396 next_batch.dedup();
1397 batch = next_batch;
1398 }
1399}
1400
1401fn discover_accessible_deps(
1405 model: &CrateModel,
1406 toolchain: &str,
1407 manifest_path: Option<&str>,
1408 target_dir: &Path,
1409 verbose: bool,
1410) -> HashSet<String> {
1411 let packages = rustdoc_json::load_lockfile_packages(manifest_path);
1412
1413 let mut batch: Vec<String> = cross_crate::collect_external_crate_names(model)
1415 .into_iter()
1416 .filter_map(|n| normalize_to_lockfile_name(&n, &packages))
1417 .collect();
1418 batch.sort();
1419 batch.dedup();
1420 let mut seen: HashSet<String> = batch.iter().cloned().collect();
1421
1422 const MAX_DEPTH: usize = 8;
1423 for _ in 0..MAX_DEPTH {
1424 if batch.is_empty() {
1425 break;
1426 }
1427
1428 let refs: Vec<&str> = batch.iter().map(|s| s.as_str()).collect();
1429 rustdoc_json::batch_generate_rustdoc_json(
1430 &refs,
1431 toolchain,
1432 manifest_path,
1433 target_dir,
1434 verbose,
1435 );
1436
1437 let mut next_batch = Vec::new();
1438 for name in &batch {
1439 let doc_dir = target_dir.join("doc");
1440 let Some(json_path) = rustdoc_json::find_lib_json_path(name, manifest_path, &doc_dir)
1441 else {
1442 continue;
1443 };
1444 let Ok(krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
1445 continue;
1446 };
1447 let sub_model = CrateModel::from_crate(krate);
1448 for sub_name in cross_crate::collect_external_crate_names(&sub_model) {
1449 if let Some(pkg_name) = normalize_to_lockfile_name(&sub_name, &packages)
1450 && !seen.contains(&pkg_name)
1451 {
1452 seen.insert(pkg_name.clone());
1453 next_batch.push(pkg_name);
1454 }
1455 }
1456 }
1457 next_batch.sort();
1458 next_batch.dedup();
1459 batch = next_batch;
1460 }
1461
1462 seen
1463}
1464
1465fn normalize_to_lockfile_name(name: &str, packages: &LockfilePackages) -> Option<String> {
1472 packages.resolve_spec(name)
1473}
1474
1475fn build_remote_crate_header(
1478 crate_name: &str,
1479 resolved_version: Option<&str>,
1480 workspace_dir: &Path,
1481 features: Option<&str>,
1482) -> Option<String> {
1483 let version = resolved_version
1484 .map(|v| v.to_string())
1485 .or_else(|| remote::resolve_crate_version(workspace_dir, crate_name))?;
1486 let mut header = format!("// crate {crate_name}[{version}]");
1487 if let Some(feats) = features {
1488 let feat_list: Vec<&str> = feats.split(',').map(|s| s.trim()).collect();
1489 let formatted = feat_list
1490 .iter()
1491 .map(|f| format!("\"{f}\""))
1492 .collect::<Vec<_>>()
1493 .join(", ");
1494 header.push_str(&format!(" features = [{formatted}]"));
1495 }
1496 Some(header)
1497}
1498
1499fn apply_glob_expansions(
1501 output: &mut String,
1502 result: &GlobExpansionResult,
1503 expand_glob: bool,
1504 filter: &FilterArgs,
1505) {
1506 if expand_glob && !result.source_models.is_empty() {
1507 let mut seen_names = HashSet::new();
1509 for source in result.item_names.keys() {
1510 if let Some(models) = result.source_models.get(source) {
1511 let mut rendered = String::new();
1512 for model in models {
1513 rendered.push_str(&render::render_inlined_items(
1514 model,
1515 filter,
1516 &mut seen_names,
1517 ));
1518 }
1519 let pattern = format!("pub use {source}::*;");
1520 replace_glob_lines(output, &pattern, &rendered);
1521 }
1522 }
1523
1524 for (source, items) in &result.named_reexports {
1526 if let Some(models) = result.source_models.get(source) {
1527 for (item_name, full_source_path) in items {
1528 if let Some(rendered) = render::render_single_inlined_item(
1529 models,
1530 item_name,
1531 filter,
1532 &mut seen_names,
1533 ) {
1534 let pattern = format!("pub use {full_source_path};");
1535 replace_glob_lines(output, &pattern, &rendered);
1536 }
1537 }
1538 }
1539 }
1540 } else if !result.item_names.is_empty() {
1541 for (source, items) in &result.item_names {
1543 let pattern = format!("pub use {source}::*;");
1544 let mut replacement = String::new();
1545 for name in items {
1546 replacement.push_str(&format!("pub use {source}::{name};\n"));
1547 }
1548 replace_glob_lines(output, &pattern, &replacement);
1549 }
1550 }
1551}
1552
1553fn replace_glob_lines(output: &mut String, pattern: &str, replacement: &str) {
1558 while let Some((start, end, indent)) = find_normalized_line(output, pattern) {
1559 let indented: String = replacement
1560 .lines()
1561 .map(|l| {
1562 if l.is_empty() {
1563 "\n".to_string()
1564 } else {
1565 format!("{indent}{l}\n")
1566 }
1567 })
1568 .collect();
1569 output.replace_range(start..end, &indented);
1570 }
1571}
1572
1573fn find_normalized_line(text: &str, pattern: &str) -> Option<(usize, usize, String)> {
1576 let mut start = 0;
1577 for line in text.split('\n') {
1578 let end = start + line.len() + 1; let normalized: String = line.split_whitespace().collect::<Vec<_>>().join(" ");
1580 if normalized == pattern {
1581 let indent = &line[..line.len() - line.trim_start().len()];
1582 return Some((start, end.min(text.len()), indent.to_string()));
1583 }
1584 start = end;
1585 }
1586 None
1587}
1588
1589fn try_generate_rustdoc_json(
1595 source: &str,
1596 toolchain: &str,
1597 manifest_path: Option<&str>,
1598 target_dir: &Path,
1599 verbose: bool,
1600 use_cache: bool,
1601) -> Option<PathBuf> {
1602 if let Ok(path) = rustdoc_json::generate_rustdoc_json(
1604 source,
1605 toolchain,
1606 manifest_path,
1607 false,
1608 target_dir,
1609 verbose,
1610 use_cache,
1611 ) {
1612 return Some(path);
1613 }
1614 let hyphenated = source.replace('_', "-");
1616 if hyphenated != source
1617 && let Ok(path) = rustdoc_json::generate_rustdoc_json(
1618 &hyphenated,
1619 toolchain,
1620 manifest_path,
1621 false,
1622 target_dir,
1623 verbose,
1624 use_cache,
1625 )
1626 {
1627 return Some(path);
1628 }
1629 None
1630}
1631
1632fn expand_glob_reexports(
1639 model: &CrateModel,
1640 target_module_path: Option<&str>,
1641 toolchain: &str,
1642 manifest_path: Option<&str>,
1643 target_dir: &Path,
1644 verbose: bool,
1645 workspace_members: &HashSet<String>,
1646) -> GlobExpansionResult {
1647 let target_item = if let Some(path) = target_module_path {
1648 model.find_module(path)
1649 } else {
1650 model.root_module()
1651 };
1652
1653 let Some(target_item) = target_item else {
1654 return GlobExpansionResult {
1655 item_names: HashMap::new(),
1656 source_models: HashMap::new(),
1657 named_reexports: HashMap::new(),
1658 };
1659 };
1660
1661 let mut item_names = HashMap::new();
1662 let mut source_models = HashMap::new();
1663
1664 for (_id, child) in model.module_children(target_item) {
1665 let ItemEnum::Use(use_item) = &child.inner else {
1666 continue;
1667 };
1668 if !use_item.is_glob {
1669 continue;
1670 }
1671
1672 let source = &use_item.source;
1673
1674 let dep_use_cache = !workspace_members.contains(source.as_str())
1676 && !workspace_members.contains(&source.replace('_', "-"));
1677
1678 let Some(json_path) = try_generate_rustdoc_json(
1680 source,
1681 toolchain,
1682 manifest_path,
1683 target_dir,
1684 verbose,
1685 dep_use_cache,
1686 ) else {
1687 continue;
1688 };
1689 let Ok(source_krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
1690 continue;
1691 };
1692
1693 let source_model = CrateModel::from_crate(source_krate);
1694 let mut all_items = Vec::new();
1695 let mut all_models = Vec::new();
1696 let mut visited = HashSet::new();
1697 visited.insert(source.clone());
1698
1699 collect_glob_items_recursive(
1700 &source_model,
1701 toolchain,
1702 manifest_path,
1703 target_dir,
1704 verbose,
1705 workspace_members,
1706 &mut visited,
1707 &mut all_items,
1708 &mut all_models,
1709 0,
1710 );
1711
1712 all_items.sort();
1713 all_items.dedup();
1714
1715 let mut models = vec![source_model];
1717 models.extend(all_models);
1718
1719 item_names.insert(source.clone(), all_items);
1720 source_models.insert(source.clone(), models);
1721 }
1722
1723 let mut named_reexports: HashMap<String, Vec<(String, String)>> = HashMap::new();
1725
1726 for (_id, child) in model.module_children(target_item) {
1727 let ItemEnum::Use(use_item) = &child.inner else {
1728 continue;
1729 };
1730 if use_item.is_glob {
1731 continue;
1732 }
1733
1734 let is_cross_crate = match &use_item.id {
1736 Some(id) => !model.krate.index.contains_key(id),
1737 None => continue, };
1739 if !is_cross_crate {
1740 continue;
1741 }
1742
1743 let source_path = &use_item.source;
1745 let Some((source_prefix, item_name)) = source_path.rsplit_once("::") else {
1746 continue;
1747 };
1748 let crate_name = source_prefix.split("::").next().unwrap();
1749
1750 if !source_models.contains_key(crate_name) {
1755 let dep_use_cache = !workspace_members.contains(crate_name)
1756 && !workspace_members.contains(&crate_name.replace('_', "-"));
1757 let Some(json_path) = try_generate_rustdoc_json(
1758 crate_name,
1759 toolchain,
1760 manifest_path,
1761 target_dir,
1762 verbose,
1763 dep_use_cache,
1764 ) else {
1765 continue;
1766 };
1767 let Ok(source_krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
1768 continue;
1769 };
1770 source_models.insert(
1771 crate_name.to_string(),
1772 vec![CrateModel::from_crate(source_krate)],
1773 );
1774 }
1775
1776 named_reexports
1777 .entry(crate_name.to_string())
1778 .or_default()
1779 .push((item_name.to_string(), source_path.clone()));
1780 }
1781
1782 GlobExpansionResult {
1783 item_names,
1784 source_models,
1785 named_reexports,
1786 }
1787}
1788
1789#[allow(clippy::too_many_arguments)]
1797fn collect_glob_items_recursive(
1798 source_model: &CrateModel,
1799 toolchain: &str,
1800 manifest_path: Option<&str>,
1801 target_dir: &Path,
1802 verbose: bool,
1803 workspace_members: &HashSet<String>,
1804 visited: &mut HashSet<String>,
1805 all_items: &mut Vec<String>,
1806 all_models: &mut Vec<CrateModel>,
1807 depth: usize,
1808) {
1809 const MAX_DEPTH: usize = 8;
1810
1811 let Some(root) = source_model.root_module() else {
1812 return;
1813 };
1814
1815 for (_, child) in source_model.module_children(root) {
1816 if !matches!(child.visibility, Visibility::Public) {
1817 continue;
1818 }
1819 if matches!(child.inner, ItemEnum::Module(_)) {
1820 continue;
1821 }
1822
1823 if let ItemEnum::Use(use_item) = &child.inner {
1824 if use_item.is_glob {
1825 if depth >= MAX_DEPTH {
1827 continue;
1828 }
1829 let nested_source = &use_item.source;
1830 if !visited.insert(nested_source.clone()) {
1831 continue; }
1833 if verbose {
1834 eprintln!(
1835 "[cargo-brief] Following nested glob re-export: {nested_source} (depth {})",
1836 depth + 1
1837 );
1838 }
1839 let nested_use_cache = !workspace_members.contains(nested_source.as_str())
1840 && !workspace_members.contains(&nested_source.replace('_', "-"));
1841 let Some(json_path) = try_generate_rustdoc_json(
1842 nested_source,
1843 toolchain,
1844 manifest_path,
1845 target_dir,
1846 verbose,
1847 nested_use_cache,
1848 ) else {
1849 continue; };
1851 let Ok(nested_krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
1852 continue;
1853 };
1854 let nested_model = CrateModel::from_crate(nested_krate);
1855 collect_glob_items_recursive(
1856 &nested_model,
1857 toolchain,
1858 manifest_path,
1859 target_dir,
1860 verbose,
1861 workspace_members,
1862 visited,
1863 all_items,
1864 all_models,
1865 depth + 1,
1866 );
1867 all_models.push(nested_model);
1868 } else {
1869 if let Some(name) = child.name.as_ref().or(Some(&use_item.name)) {
1871 all_items.push(name.clone());
1872 }
1873 }
1874 } else {
1875 if let Some(name) = &child.name {
1877 all_items.push(name.clone());
1878 }
1879 }
1880 }
1881}