1#![forbid(unsafe_code)]
2
3mod callbacks;
4mod check_release;
5mod config;
6mod data_generation;
7mod manifest;
8mod query;
9mod rustdoc_gen;
10mod templating;
11mod util;
12mod witness_gen;
13
14use anyhow::Context;
15use cargo_metadata::PackageId;
16use clap::ValueEnum;
17use data_generation::{DataStorage, IntoTerminalResult as _, TerminalError};
18use directories::ProjectDirs;
19use itertools::Itertools;
20use serde::Serialize;
21
22use std::collections::{BTreeMap, HashSet};
23use std::io::Write as _;
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26
27use check_release::{CheckReleaseSettings, LintResult, PendingCrateReport, run_check_release};
28use rustdoc_gen::CrateDataForRustdoc;
29
30pub use config::{FeatureFlag, GlobalConfig};
31pub use query::{
32 ActualSemverUpdate, LintLevel, OverrideMap, OverrideStack, QueryOverride, RequiredSemverUpdate,
33 SemverQuery, Witness, WitnessPurpose,
34};
35
36#[non_exhaustive]
38#[derive(Debug, PartialEq, Eq, Serialize)]
39pub struct Check {
40 scope: Scope,
42 current: Rustdoc,
43 baseline: Rustdoc,
44
45 #[serde(skip_serializing_if = "RustdocIndexingMode::is_ordinary")]
49 rustdoc_indexing_mode: RustdocIndexingMode,
50
51 release_type: Option<ReleaseType>,
52 current_feature_config: rustdoc_gen::FeatureConfig,
53 baseline_feature_config: rustdoc_gen::FeatureConfig,
54 build_target: Option<String>,
56 witness_generation: WitnessGeneration,
58}
59
60#[non_exhaustive]
65#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
66pub enum ReleaseType {
67 Major,
68 Minor,
69 Patch,
70}
71
72#[non_exhaustive]
73#[derive(Debug, PartialEq, Eq, Serialize)]
74pub struct Rustdoc {
75 source: RustdocSource,
76}
77
78#[doc(hidden)]
80#[non_exhaustive]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
82pub enum RustdocIndexingMode {
83 #[default]
85 Ordinary,
86 StabilityAware,
88}
89
90impl RustdocIndexingMode {
91 fn is_ordinary(&self) -> bool {
92 matches!(self, Self::Ordinary)
93 }
94}
95
96impl Rustdoc {
97 pub fn from_path(rustdoc_path: impl Into<PathBuf>) -> Self {
99 Self {
100 source: RustdocSource::Rustdoc(rustdoc_path.into()),
101 }
102 }
103
104 pub fn from_root(project_root: impl Into<PathBuf>) -> Self {
109 Self {
110 source: RustdocSource::Root(project_root.into()),
111 }
112 }
113
114 pub fn from_git_revision(
116 project_root: impl Into<PathBuf>,
117 revision: impl Into<String>,
118 ) -> Self {
119 Self {
120 source: RustdocSource::Revision(project_root.into(), revision.into()),
121 }
122 }
123
124 pub fn from_registry_latest_crate_version() -> Self {
128 Self {
129 source: RustdocSource::VersionFromRegistry(None),
130 }
131 }
132
133 pub fn from_registry(crate_version: impl Into<String>) -> Self {
135 Self {
136 source: RustdocSource::VersionFromRegistry(Some(crate_version.into())),
137 }
138 }
139}
140
141#[derive(Debug, PartialEq, Eq, Serialize)]
142enum RustdocSource {
143 Rustdoc(PathBuf),
146 Root(PathBuf),
149 Revision(PathBuf, String),
151 VersionFromRegistry(Option<String>),
156}
157
158#[derive(Default, Debug, PartialEq, Eq, Serialize)]
160struct Scope {
161 mode: ScopeMode,
162}
163
164#[derive(Debug, PartialEq, Eq, Serialize)]
165enum ScopeMode {
166 DenyList(PackageSelection),
168 AllowList(Vec<String>),
170}
171
172impl Default for ScopeMode {
173 fn default() -> Self {
174 Self::DenyList(PackageSelection::default())
175 }
176}
177
178#[non_exhaustive]
179#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)]
180pub struct PackageSelection {
181 selection: ScopeSelection,
182 explicitly_included_packages: Vec<String>,
183 excluded_packages: Vec<String>,
184}
185
186impl PackageSelection {
187 pub fn new(selection: ScopeSelection) -> Self {
188 Self {
189 selection,
190 explicitly_included_packages: vec![],
191 excluded_packages: vec![],
192 }
193 }
194
195 pub fn set_explicitly_included_packages(&mut self, packages: Vec<String>) -> &mut Self {
196 self.explicitly_included_packages = packages;
197 self
198 }
199
200 pub fn set_excluded_packages(&mut self, packages: Vec<String>) -> &mut Self {
201 self.excluded_packages = packages;
202 self
203 }
204}
205
206#[non_exhaustive]
207#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize)]
208pub enum ScopeSelection {
209 Workspace,
211 #[default]
213 DefaultMembers,
214}
215
216impl Scope {
217 fn selected_packages<'m>(
219 &self,
220 meta: &'m cargo_metadata::Metadata,
221 ) -> (
222 Vec<&'m cargo_metadata::Package>,
223 Vec<&'m cargo_metadata::Package>,
224 ) {
225 let workspace_members: HashSet<&PackageId> = meta.workspace_members.iter().collect();
226 let base_ids: HashSet<&PackageId> = match &self.mode {
227 ScopeMode::DenyList(PackageSelection {
228 selection,
229 explicitly_included_packages: _,
230 excluded_packages,
231 }) => {
232 let packages = match selection {
233 ScopeSelection::Workspace => workspace_members,
234 ScopeSelection::DefaultMembers => {
235 let resolve = meta.resolve.as_ref().expect("no-deps is unsupported");
237 match &resolve.root {
238 Some(root) => {
239 let mut base_ids = HashSet::new();
240 base_ids.insert(root);
241 base_ids
242 }
243 None => workspace_members,
244 }
245 }
246 };
247
248 packages
249 .iter()
250 .filter(|p| !excluded_packages.contains(&meta[p].name))
251 .copied()
252 .collect()
253 }
254 ScopeMode::AllowList(patterns) => {
255 meta.packages
256 .iter()
257 .filter(|p| workspace_members.contains(&p.id) && patterns.contains(&p.name))
260 .map(|p| &p.id)
261 .collect()
262 }
263 };
264
265 meta.packages
266 .iter()
267 .filter(|&p| {
268 base_ids.contains(&p.id)
270 })
271 .partition(|&p| p.targets.iter().any(is_lib_like_checkable_target))
272 }
273}
274
275struct CrateToCheck<'a> {
276 overrides: OverrideStack,
277 current_crate_data: CrateDataForRustdoc<'a>,
278 baseline_crate_data: CrateDataForRustdoc<'a>,
279}
280
281#[expect(
286 clippy::unneeded_struct_pattern,
287 reason = "we don't want a breaking change if the target variants change from unit variants to a different kind"
288)]
289fn is_lib_like_checkable_target(target: &cargo_metadata::Target) -> bool {
290 target.is_lib()
291 || target.kind.iter().any(|kind| {
292 matches!(
293 kind,
294 cargo_metadata::TargetKind::RLib { .. }
295 | cargo_metadata::TargetKind::DyLib { .. }
296 | cargo_metadata::TargetKind::CDyLib { .. }
297 | cargo_metadata::TargetKind::StaticLib { .. }
298 )
299 })
300}
301
302impl Check {
303 pub fn new(current: Rustdoc) -> Self {
304 Self {
305 scope: Scope::default(),
306 current,
307 baseline: Rustdoc::from_registry_latest_crate_version(),
308 rustdoc_indexing_mode: RustdocIndexingMode::default(),
309 release_type: None,
310 current_feature_config: rustdoc_gen::FeatureConfig::default_for_current(),
311 baseline_feature_config: rustdoc_gen::FeatureConfig::default_for_baseline(),
312 build_target: None,
313 witness_generation: WitnessGeneration::default(),
314 }
315 }
316
317 pub fn set_package_selection(&mut self, selection: PackageSelection) -> &mut Self {
318 self.scope.mode = ScopeMode::DenyList(selection);
319 self
320 }
321
322 pub fn set_packages(&mut self, packages: Vec<String>) -> &mut Self {
323 self.scope.mode = ScopeMode::AllowList(packages);
324 self
325 }
326
327 pub fn set_baseline(&mut self, baseline: Rustdoc) -> &mut Self {
328 self.baseline = baseline;
329 self
330 }
331
332 pub fn set_release_type(&mut self, release_type: ReleaseType) -> &mut Self {
333 self.release_type = Some(release_type);
334 self
335 }
336
337 #[doc(hidden)]
338 pub fn set_rustdoc_indexing_mode(&mut self, mode: RustdocIndexingMode) -> &mut Self {
339 self.rustdoc_indexing_mode = mode;
340 self
341 }
342
343 pub fn with_only_explicit_features(&mut self) -> &mut Self {
344 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::None;
345 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::None;
346 self
347 }
348
349 pub fn with_default_features(&mut self) -> &mut Self {
350 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::Default;
351 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::Default;
352 self
353 }
354
355 pub fn with_heuristically_included_features(&mut self) -> &mut Self {
356 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::Heuristic;
357 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::Heuristic;
358 self
359 }
360
361 pub fn with_all_features(&mut self) -> &mut Self {
362 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::All;
363 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::All;
364 self
365 }
366
367 pub fn set_extra_features(
368 &mut self,
369 extra_current_features: Vec<String>,
370 extra_baseline_features: Vec<String>,
371 ) -> &mut Self {
372 self.current_feature_config.extra_features = extra_current_features;
373 self.baseline_feature_config.extra_features = extra_baseline_features;
374 self
375 }
376
377 pub fn set_build_target(&mut self, build_target: String) -> &mut Self {
380 self.build_target = Some(build_target);
381 self
382 }
383
384 pub fn set_witness_generation(&mut self, witness_generation: WitnessGeneration) -> &mut Self {
386 self.witness_generation = witness_generation;
387 self
388 }
389
390 fn get_target_dir(&self, source: &RustdocSource) -> anyhow::Result<PathBuf> {
398 Ok(
399 if let Some(path) = get_target_dir_from_project_root(source)? {
400 path
401 } else if let Some(path) = get_target_dir_from_project_root(&self.current.source)? {
402 path
403 } else if let Some(path) = get_target_dir_from_project_root(&self.baseline.source)? {
404 path
405 } else {
406 get_cache_dir()?
407 },
408 )
409 }
410
411 fn get_rustdoc_generator(
412 &self,
413 config: &mut GlobalConfig,
414 source: &RustdocSource,
415 ) -> anyhow::Result<rustdoc_gen::RustdocGenerator> {
416 let target_dir = self.get_target_dir(source)?;
417 Ok(match source {
418 RustdocSource::Rustdoc(path) => {
419 rustdoc_gen::RustdocFromFile::new(path.to_owned()).into()
420 }
421 RustdocSource::Root(root) => {
422 rustdoc_gen::RustdocFromProjectRoot::new(root, &target_dir)?.into()
423 }
424 RustdocSource::Revision(root, rev) => {
425 let metadata = manifest_metadata_no_deps(root)?;
426 let source = metadata.workspace_root.as_std_path();
427 rustdoc_gen::RustdocFromGitRevision::with_rev(source, &target_dir, rev, config)?
428 .into()
429 }
430 RustdocSource::VersionFromRegistry(version) => {
431 let mut registry = rustdoc_gen::RustdocFromRegistry::new(&target_dir, config)?;
432 if let Some(ver) = version {
433 let semver = semver::Version::parse(ver)?;
434 registry.set_version(semver);
435 }
436 registry.into()
437 }
438 })
439 }
440
441 pub fn check_release(&self, config: &mut GlobalConfig) -> anyhow::Result<Report> {
442 let generation_settings = data_generation::GenerationSettings {
443 use_color: config.err_color_choice(),
444 pass_through_stderr: config.is_verbose(),
445 };
446
447 if !(matches!(self.current.source, RustdocSource::Rustdoc(_))
450 && matches!(self.baseline.source, RustdocSource::Rustdoc(_)))
451 {
452 let rustc_version_needed = config.minimum_rustc_version();
453 match rustc_version::version() {
454 Ok(rustc_version) => {
455 if rustc_version < *rustc_version_needed {
456 let help = "HELP: to use the latest rustc, run `rustup update stable && cargo +stable semver-checks <args>`";
457 anyhow::bail!(
458 "rustc version is not high enough: >={rustc_version_needed} needed, got {rustc_version}\n\n{help}"
459 );
460 }
461 }
462 Err(error) => {
463 let help = format!(
464 "HELP: to avoid errors please ensure rustc >={rustc_version_needed} is used"
465 );
466 config.shell_warn(format_args!(
467 "failed to determine the current rustc version: {error}\n\n{help}"
468 ))?;
469 }
470 };
471 }
472
473 let crates_to_check: Vec<CrateToCheck<'_>> = match &self.current.source {
474 RustdocSource::Rustdoc(_)
475 | RustdocSource::Revision(_, _)
476 | RustdocSource::VersionFromRegistry(_) => {
477 let names = match &self.scope.mode {
478 ScopeMode::DenyList(_) => match &self.current.source {
479 RustdocSource::Rustdoc(_) => {
480 vec!["<unknown>".to_string()]
484 }
485 _ => anyhow::bail!(
486 "couldn't deduce crate name, specify one through the package allow list"
487 ),
488 },
489 ScopeMode::AllowList(lst) => lst.clone(),
490 };
491 names
492 .into_iter()
493 .map(|name| {
494 let version = None;
495 CrateToCheck {
496 overrides: OverrideStack::new(),
497 current_crate_data: CrateDataForRustdoc {
498 crate_type: rustdoc_gen::CrateType::Current,
499 name: name.clone(),
500 feature_config: &self.current_feature_config,
501 build_target: self.build_target.as_deref(),
502 },
503 baseline_crate_data: CrateDataForRustdoc {
504 crate_type: rustdoc_gen::CrateType::Baseline {
505 highest_allowed_version: version,
506 },
507 name,
508 feature_config: &self.baseline_feature_config,
509 build_target: self.build_target.as_deref(),
510 },
511 }
512 })
513 .collect()
514 }
515 RustdocSource::Root(project_root) => {
516 let metadata = manifest_metadata(project_root)?;
517 let (selected, skipped) = self.scope.selected_packages(&metadata);
518 if selected.is_empty() {
519 let help = if skipped.is_empty() {
520 "".to_string()
521 } else {
522 let skipped = skipped.iter().map(|&p| &p.name).join(", ");
523 format!(
524 "
525note: only library targets contain an API surface that can be checked for semver
526note: skipped the following crates since they have no library target: {skipped}"
527 )
528 };
529 anyhow::bail!(
530 "no crates with library targets selected, nothing to semver-check{help}"
531 );
532 }
533
534 let workspace_overrides =
535 manifest::deserialize_lint_table(&metadata.workspace_metadata)
536 .context("[workspace.metadata.cargo-semver-checks] table is invalid")?
537 .map(|table| table.into_stack());
538
539 selected
540 .iter()
541 .map(|selected| {
542 let crate_name = &selected.name;
543 let version = &selected.version;
544
545 let is_explicitly_included = match &self.scope.mode {
550 ScopeMode::AllowList(packages) => packages.contains(&selected.name),
551 ScopeMode::DenyList(PackageSelection {
552 explicitly_included_packages,
553 ..
554 }) => explicitly_included_packages.contains(&selected.name),
555 };
556 let is_implied = !is_explicitly_included
557 && metadata.workspace_members.len() > 1
558 && selected.publish == Some(vec![]);
559 if is_implied {
560 config.log_verbose(|config| {
561 config.shell_status(
562 "Skipping",
563 format_args!("{crate_name} v{version} (current)"),
564 )
565 })?;
566 Ok(None)
567 } else {
568 let overrides = overrides_for_workspace_package(
569 selected,
570 workspace_overrides.as_deref(),
571 )?;
572
573 Ok(Some(CrateToCheck {
574 overrides,
575 current_crate_data: CrateDataForRustdoc {
576 crate_type: rustdoc_gen::CrateType::Current,
577 name: crate_name.to_string(),
578 feature_config: &self.current_feature_config,
579 build_target: self.build_target.as_deref(),
580 },
581 baseline_crate_data: CrateDataForRustdoc {
582 crate_type: rustdoc_gen::CrateType::Baseline {
583 highest_allowed_version: Some(version.clone()),
584 },
585 name: crate_name.to_string(),
586 feature_config: &self.baseline_feature_config,
587 build_target: self.build_target.as_deref(),
588 },
589 }))
590 }
591 })
592 .filter_map(|res| res.transpose())
593 .collect::<Result<Vec<_>, anyhow::Error>>()?
594 }
595 };
596
597 let current_loader = self.get_rustdoc_generator(config, &self.current.source)?;
598 let baseline_loader = self.get_rustdoc_generator(config, &self.baseline.source)?;
599 let witness_target_dir = self.get_target_dir(&self.current.source)?;
600
601 let all_outcomes: Vec<anyhow::Result<(String, PendingCrateReport)>> = crates_to_check
604 .into_iter()
605 .map(|selected| {
606 let start = std::time::Instant::now();
607 let name = selected.current_crate_data.name.clone();
608
609 let current_loader = rustdoc_gen::StatefulRustdocGenerator::couple_data(
610 ¤t_loader,
611 config,
612 &selected.current_crate_data,
613 )
614 .map_err(|err| log_terminal_error(config, err))?;
615 let baseline_loader = rustdoc_gen::StatefulRustdocGenerator::couple_data(
616 &baseline_loader,
617 config,
618 &selected.baseline_crate_data,
619 )
620 .map_err(|err| log_terminal_error(config, err))?;
621
622 let current_loader = current_loader
623 .prepare_generator(config)
624 .map_err(|err| log_terminal_error(config, err))?;
625 let baseline_loader = baseline_loader
626 .prepare_generator(config)
627 .map_err(|err| log_terminal_error(config, err))?;
628
629 let witness_data = witness_gen::WitnessGenerationData::new(
630 baseline_loader.get_data_request(),
631 current_loader.get_data_request(),
632 witness_target_dir.clone(),
633 );
634
635 let data_storage = generate_crate_data(
636 config,
637 generation_settings,
638 ¤t_loader,
639 &baseline_loader,
640 )
641 .map_err(|err| log_terminal_error(config, err))?;
642
643 let report = run_check_release(
644 config,
645 &data_storage,
646 &name,
647 CheckReleaseSettings {
648 release_type: self.release_type,
649 rustdoc_indexing_mode: self.rustdoc_indexing_mode,
650 },
651 &selected.overrides,
652 &self.witness_generation,
653 witness_data,
654 )?;
655 config.shell_status(
656 "Finished",
657 format_args!("[{:>8.3}s] {name}", start.elapsed().as_secs_f32()),
658 )?;
659 Ok((name, report))
660 })
661 .collect();
662 let crate_reports: BTreeMap<String, CrateReport> = {
663 let mut reports = BTreeMap::new();
664 let mut witness_run_reports = Vec::new();
665 for outcome in all_outcomes {
666 let (name, outcome) = outcome?;
667 witness_run_reports.push(outcome.witness_run_report);
668 reports.insert(name, outcome.report);
669 }
670
671 match witness_gen::finalize_retained_artifacts(config.run_id(), &witness_run_reports) {
672 Ok(retained_artifact_dirs) => {
673 if !retained_artifact_dirs.is_empty() {
674 if retained_artifact_dirs.len() == 1 {
675 config.shell_note(format_args!(
676 "retained witness artifacts in {}",
677 retained_artifact_dirs[0].display()
678 ))?;
679 } else {
680 config.shell_note("retained witness artifacts in:")?;
681 for dir in retained_artifact_dirs {
682 writeln!(config.stderr(), "{:12}{}", "", dir.display())?;
683 }
684 }
685 }
686 }
687 Err(error) => {
688 config.shell_warn(format_args!(
689 "failed to retain witness artifacts: {error:#}"
690 ))?;
691 }
692 }
693 reports
694 };
695
696 Ok(Report { crate_reports })
697 }
698}
699
700fn overrides_for_workspace_package(
701 package: &cargo_metadata::Package,
702 workspace_overrides: Option<&[BTreeMap<String, QueryOverride>]>,
703) -> Result<OverrideStack, anyhow::Error> {
704 let lint_table = manifest::deserialize_lint_table(&package.metadata).with_context(|| {
705 format!(
706 "package `{}`'s [package.metadata.cargo-semver-checks] table is invalid (at {})",
707 package.name, package.manifest_path,
708 )
709 })?;
710 let selected_manifest =
711 manifest::Manifest::parse_standalone(package.manifest_path.clone().into_std_path_buf())?;
712
713 let use_workspace_lints = matches!(
715 selected_manifest.parsed.lints,
716 cargo_toml::Inheritable::Inherited
717 );
718 let metadata_workspace_key = lint_table.as_ref().is_some_and(|x| x.workspace);
719
720 let mut overrides = OverrideStack::new();
721 if (use_workspace_lints || metadata_workspace_key)
722 && let Some(workspace) = workspace_overrides
723 {
724 for level in workspace {
725 overrides.push(level);
726 }
727 }
728 if let Some(lint_table) = lint_table {
729 for level in lint_table.into_stack() {
730 overrides.push(&level);
731 }
732 }
733 Ok(overrides)
734}
735
736#[cold]
737fn log_terminal_error(config: &mut GlobalConfig, err: TerminalError) -> anyhow::Error {
738 match err {
739 TerminalError::WithAdvice(err, advice) => {
740 if let Err(err) = config.log_error(|config| {
741 writeln!(config.stderr(), "{advice}")?;
742 Ok(())
743 }) {
744 return err;
745 }
746 err
747 }
748 TerminalError::Other(err) => err,
749 }
750}
751
752#[derive(Debug)]
754struct Bumps {
755 major: u32,
756 minor: u32,
757}
758
759impl Bumps {
760 pub fn update_type(&self) -> Option<RequiredSemverUpdate> {
764 if self.major > 0 {
765 Some(RequiredSemverUpdate::Major)
766 } else if self.minor > 0 {
767 Some(RequiredSemverUpdate::Minor)
768 } else {
769 None
770 }
771 }
772}
773
774#[non_exhaustive]
776#[derive(Debug)]
777pub struct CrateReport {
778 detected_bump: ActualSemverUpdate,
780 required_bumps: Bumps,
782 suggested_bumps: Bumps,
784 lint_results: Vec<LintResult>,
786 checks_duration: Duration,
788 selected_checks: usize,
790 skipped_checks: usize,
792 witness_statistics: Option<WitnessStatistics>,
794}
795
796impl CrateReport {
797 pub fn success(&self) -> bool {
804 match self.required_bumps.update_type().map(ReleaseType::from) {
805 None => true,
807 Some(required_bump) => {
809 match self.detected_bump {
812 ActualSemverUpdate::Major => {
815 panic!("detected_bump is major, while required_bump is {required_bump:?}")
816 }
817 ActualSemverUpdate::Minor => {
818 assert_eq!(required_bump, ReleaseType::Major);
819 }
820 ActualSemverUpdate::Patch | ActualSemverUpdate::NotChanged => {
821 assert!(matches!(
822 required_bump,
823 ReleaseType::Major | ReleaseType::Minor
824 ));
825 }
826 }
827 false
828 }
829 }
830 }
831
832 pub fn has_required_witness_errors(&self) -> bool {
834 self.witness_statistics
835 .as_ref()
836 .is_some_and(|statistics| statistics.required_witness_errors() > 0)
837 }
838
839 pub fn required_bump(&self) -> Option<ReleaseType> {
842 self.required_bumps.update_type().map(ReleaseType::from)
843 }
844
845 pub fn detected_bump(&self) -> ActualSemverUpdate {
847 self.detected_bump
848 }
849
850 pub fn witness_statistics(&self) -> Option<&WitnessStatistics> {
852 self.witness_statistics.as_ref()
853 }
854}
855
856#[non_exhaustive]
859#[derive(Debug)]
860pub struct Report {
861 crate_reports: BTreeMap<String, CrateReport>,
863}
864
865impl Report {
866 pub fn success(&self) -> bool {
869 self.crate_reports.values().all(|report| report.success())
870 }
871
872 pub fn has_required_witness_errors(&self) -> bool {
874 self.crate_reports
875 .values()
876 .any(|report| report.has_required_witness_errors())
877 }
878
879 pub fn crate_reports(&self) -> &BTreeMap<String, CrateReport> {
881 &self.crate_reports
882 }
883}
884
885#[non_exhaustive]
890#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
891pub struct WitnessGeneration {
892 pub show_hints: bool,
895 pub run_consistency_checks: bool,
899}
900
901impl WitnessGeneration {
902 #[inline]
904 #[must_use]
905 pub const fn new() -> Self {
906 Self {
907 show_hints: false,
908 run_consistency_checks: false,
909 }
910 }
911}
912
913#[non_exhaustive]
915#[derive(Debug, Clone, PartialEq, Eq)]
916pub struct WitnessStatistics {
917 not_confirmed_by_witness: usize,
918 consistency_check_mismatches: usize,
919 consistency_check_errors: usize,
920 required_witness_errors: usize,
921}
922
923impl WitnessStatistics {
924 pub(crate) const fn new(
925 not_confirmed_by_witness: usize,
926 consistency_check_mismatches: usize,
927 consistency_check_errors: usize,
928 required_witness_errors: usize,
929 ) -> Self {
930 Self {
931 not_confirmed_by_witness,
932 consistency_check_mismatches,
933 consistency_check_errors,
934 required_witness_errors,
935 }
936 }
937
938 pub(crate) fn is_empty(&self) -> bool {
939 self.not_confirmed_by_witness == 0
940 && self.consistency_check_mismatches == 0
941 && self.consistency_check_errors == 0
942 && self.required_witness_errors == 0
943 }
944
945 pub fn not_confirmed_by_witness(&self) -> usize {
949 self.not_confirmed_by_witness
950 }
951
952 pub fn consistency_check_mismatches(&self) -> usize {
955 self.consistency_check_mismatches
956 }
957
958 pub fn consistency_check_errors(&self) -> usize {
961 self.consistency_check_errors
962 }
963
964 pub fn required_witness_errors(&self) -> usize {
967 self.required_witness_errors
968 }
969}
970
971fn generate_crate_data(
972 config: &mut GlobalConfig,
973 generation_settings: data_generation::GenerationSettings,
974 current_loader: &rustdoc_gen::StatefulRustdocGenerator<'_, rustdoc_gen::ReadyState<'_>>,
975 baseline_loader: &rustdoc_gen::StatefulRustdocGenerator<'_, rustdoc_gen::ReadyState<'_>>,
976) -> Result<DataStorage, TerminalError> {
977 let current_crate = current_loader.load_rustdoc(
978 config,
979 generation_settings,
980 data_generation::CacheSettings::ReadWrite(()),
981 )?;
982
983 let baseline_crate_name = &baseline_loader.get_crate_data().name;
984 let current_rustdoc_version = current_crate.version();
985
986 let baseline_crate = {
987 let mut baseline_crate = baseline_loader.load_rustdoc(
988 config,
989 generation_settings,
990 data_generation::CacheSettings::ReadWrite(()),
991 )?;
992
993 if baseline_crate.version() != current_rustdoc_version {
1001 let crate_name = baseline_crate_name;
1002 config
1003 .shell_status(
1004 "Removing",
1005 format_args!("stale cached baseline rustdoc for {crate_name}"),
1006 )
1007 .into_terminal_result()?;
1008
1009 baseline_crate = baseline_loader.load_rustdoc(
1010 config,
1011 generation_settings,
1012 data_generation::CacheSettings::WriteOnly(()),
1013 )?;
1014
1015 assert_eq!(
1016 baseline_crate.version(),
1017 current_rustdoc_version,
1018 "Deleting and regenerating the baseline JSON file did not resolve the rustdoc \
1019 version mismatch."
1020 );
1021 }
1022
1023 baseline_crate
1024 };
1025
1026 Ok(DataStorage::new(current_crate, baseline_crate))
1027}
1028
1029fn manifest_path(project_root: &Path) -> anyhow::Result<PathBuf> {
1030 if project_root.is_dir() {
1031 let manifest_path = project_root.join("Cargo.toml");
1032 if manifest_path.exists() {
1036 Ok(manifest_path)
1037 } else {
1038 anyhow::bail!(
1039 "couldn't find Cargo.toml in directory {}",
1040 project_root.display()
1041 )
1042 }
1043 } else if project_root.ends_with("Cargo.toml") {
1044 Ok(project_root.to_path_buf())
1048 } else {
1049 anyhow::bail!(
1050 "path '{}' is not a directory or a manifest",
1051 project_root.display()
1052 )
1053 }
1054}
1055
1056fn manifest_metadata(project_root: &Path) -> anyhow::Result<cargo_metadata::Metadata> {
1057 let manifest_path = manifest_path(project_root)?;
1058 let mut command = cargo_metadata::MetadataCommand::new();
1059 let metadata = command.manifest_path(manifest_path).exec()?;
1060 Ok(metadata)
1061}
1062
1063fn manifest_metadata_no_deps(project_root: &Path) -> anyhow::Result<cargo_metadata::Metadata> {
1064 let manifest_path = manifest_path(project_root)?;
1065 let mut command = cargo_metadata::MetadataCommand::new();
1066 let metadata = command.manifest_path(manifest_path).no_deps().exec()?;
1067 Ok(metadata)
1068}
1069
1070fn get_cache_dir() -> anyhow::Result<PathBuf> {
1071 let project_dirs =
1072 ProjectDirs::from("", "", "cargo-semver-checks").context("can't determine project dirs")?;
1073 let cache_dir = project_dirs.cache_dir();
1074 std::fs::create_dir_all(cache_dir).context("can't create cache dir")?;
1075 Ok(cache_dir.to_path_buf())
1076}
1077
1078fn get_target_dir_from_project_root(source: &RustdocSource) -> anyhow::Result<Option<PathBuf>> {
1079 Ok(match source {
1080 RustdocSource::Root(root) => {
1081 let metadata = manifest_metadata_no_deps(root)?;
1082 let target = metadata.target_directory.as_std_path().join(util::SCOPE);
1083 Some(target)
1084 }
1085 RustdocSource::Revision(root, rev) => {
1086 let metadata = manifest_metadata_no_deps(root)?;
1087 let target = metadata.target_directory.as_std_path().join(util::SCOPE);
1088 let target = target.join(format!("git-{}", util::slugify(rev)));
1089 Some(target)
1090 }
1091 RustdocSource::Rustdoc(_path) => None,
1092 RustdocSource::VersionFromRegistry(_version) => None,
1093 })
1094}