cargo-public-api 0.52.0

List and diff the public API of Rust library crates between releases and commits. Detect breaking API changes and semver violations via CI or a CLI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
use std::ffi::OsString;
use std::io::{stderr, stdout};
use std::path::{Path, PathBuf};

use anyhow::{Result, anyhow, bail};
use api_source::{ApiSource, Commit, CurrentDir, PublishedCrate, RustdocJson};
use arg_types::{Color, DenyMethod, Include, Omit};
use git_utils::current_branch_or_commit;
use plain::Plain;
use public_api::diff::PublicApiDiff;

use clap::{CommandFactory, Parser};

mod api_source;
mod arg_types;
mod error;
mod git_utils;
mod plain;
mod published_crate;
mod toolchain;

#[derive(Parser, Debug)]
#[command(
    author,
    version,
    about = "List and diff the public API of Rust library crates between releases and commits.",
    long_about = "List and diff the public API of Rust library crates between releases and commits. Website: https://github.com/cargo-public-api/cargo-public-api",
    bin_name = "cargo public-api"
)]
#[command(flatten_help = true)]
pub struct Args {
    /// Path to `Cargo.toml`.
    #[arg(global = true, long, value_name = "PATH", default_value = "Cargo.toml")]
    manifest_path: PathBuf,

    /// Name of package in workspace to list or diff the public API for.
    #[arg(global = true, long, short)]
    package: Option<String>,

    /// Omit noisy items.
    #[arg(global = true, long, value_enum, value_delimiter = ',')]
    omit: Option<Vec<Omit>>,

    /// Shorthand for omitting noisy items. Can be used more than once.
    ///
    /// | Usage | Corresponds to                                           |
    /// |-------|----------------------------------------------------------|
    /// | -s    | --omit blanket-impls                                     |
    /// | -ss   | --omit blanket-impls,auto-trait-impls                    |
    /// | -sss  | --omit blanket-impls,auto-trait-impls,auto-derived-impls |
    #[clap(verbatim_doc_comment)]
    #[arg(global = true, short, long, action = clap::ArgAction::Count)]
    simplified: u8,

    /// Include extra details.
    #[arg(global = true, long, value_enum, value_delimiter = ',')]
    include: Option<Vec<Include>>,

    /// Shorthand for including extra details.
    ///
    /// | Usage | Corresponds to                                           |
    /// |-------|----------------------------------------------------------|
    /// | -v    | --include function-parameter-names                       |
    #[clap(verbatim_doc_comment)]
    #[arg(global = true, short, long, action = clap::ArgAction::Count)]
    verbose: u8,

    /// Space or comma separated list of features to activate
    #[arg(global = true, long, short = 'F')]
    features: Vec<String>,

    /// Activate all available features
    #[arg(global = true, long)]
    all_features: bool,

    /// Do not activate the `default` feature
    #[arg(global = true, long)]
    no_default_features: bool,

    /// Build for the target triple
    #[arg(global = true, long)]
    target: Option<String>,

    /// When to color the output.
    ///
    /// By default, `--color=auto` is active. Using just `--color` without an
    /// arg is equivalent to `--color=always`.
    #[arg(global = true, long, value_enum)]
    color: Option<Option<Color>>,

    /// List the public API based on the given rustdoc JSON file.
    ///
    /// Example:
    ///
    /// First do
    ///
    ///     rustup component add rust-docs-json --toolchain nightly
    ///
    /// and then
    ///
    ///     cargo public-api --rustdoc-json ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/share/doc/rust/json/std.json
    ///
    #[arg(global = true, long, value_name = "RUSTDOC_JSON_PATH", hide = true)]
    rustdoc_json: Option<String>,

    /// Show detailed info about processing.
    ///
    /// Only intended for debugging this tool.
    #[arg(global = true, long, hide = true)]
    debug_processing: bool,

    /// Show the hidden "sorting prefix" that makes items nicely grouped
    ///
    /// Only intended for debugging this tool.
    #[arg(global = true, long, hide = true)]
    debug_sorting: bool,

    /// Where to put rustdoc JSON build artifacts.
    ///
    /// Hidden by default because it will typically not be needed by users.
    /// Mainly useful to allow tests to run in parallel.
    #[arg(global = true, long, value_name = "PATH", hide = true)]
    target_dir: Option<PathBuf>,

    /// Forwarded to rustdoc JSON build command
    #[arg(global = true, long, hide = true)]
    cap_lints: Option<String>,

    #[command(subcommand)]
    subcommand: Option<Subcommand>,
}

/// We don't want `toolchain` in [Args] because we only support the `cargo
/// +toolchain public-api` way of picking toolchain. But we still want to
/// resolve the toolchain to use once and pass it around a bit. This helper
/// structs solves this for us.
#[derive(Debug)]
struct ArgsAndToolchain {
    args: Args,
    toolchain: Option<String>,
}

/// The subcommand used for diffing.
#[derive(Parser, Debug)]
struct DiffArgs {
    /// Exit with failure if the specified API diff is detected.
    ///
    /// Can be combined. For example, to only allow additions to the API, use
    /// `--deny=added --deny=changed`.
    #[arg(long, value_enum)]
    deny: Option<Vec<DenyMethod>>,

    /// Force the diff. For example, when diffing commits, enabling this option
    /// will discard working tree changes during git checkouts of other commits.
    #[arg(long)]
    force: bool,

    #[clap(verbatim_doc_comment)]
    /// What to diff.
    ///
    /// EXAMPLES
    /// ========
    ///
    /// Diff current working tree version of `specific-crate` against published version 1.2.3:
    ///
    ///     cargo public-api -p specific-crate diff 1.2.3
    ///
    /// Diff between commits:
    ///
    ///     cargo public-api diff v0.2.0..v0.3.0
    ///
    /// See
    ///
    ///     cargo public-api diff --help
    ///
    /// for more examples and more info.
    args: Vec<String>,
}

#[derive(clap::Subcommand, Debug)]
enum Subcommand {
    /// Diff the public API against a published version of the crate, or between commits.
    ///
    /// If the diffing
    ///
    /// * arg looks like a specific version string (`x.y.z`) then the diff will be between that published
    ///   version of the crate and the working directory.
    ///
    /// * arg has `..` like in `tag1..tag2` then the public API of each individual git commit will be
    ///   diffed. See below for how that works.
    ///
    /// * args end with `.json` like in `file1.json file2.json` then rustdoc JSON file diffing will be
    ///   performed.
    ///
    ///
    /// EXAMPLES:
    /// =========
    ///
    /// Diff a published version of a crate against the current working tree:
    ///
    ///     cargo public-api diff 1.2.3
    ///
    /// Diff the latest version of a crate against the current working tree:
    ///
    ///     cargo public-api diff latest
    ///
    /// Diff between two published versions of any crate:
    ///
    ///     cargo public-api -p example_api diff 0.1.0 0.2.0
    ///
    /// Diff current working tree version of `specific-crate` against published version 1.2.3:
    ///
    ///     cargo public-api -p specific-crate diff 1.2.3
    ///
    /// Diff between commits:
    ///
    ///     cargo public-api diff v0.2.0..v0.3.0
    ///
    /// Diff between rustdoc JSON files:
    ///
    ///     cargo public-api diff first.json second.json
    ///
    ///
    /// HOW COMMIT DIFFING WORKS:
    /// =========================
    ///
    /// When diffing commits, the following steps are performed:
    ///
    /// 1. Remember the current branch/commit
    /// 2. Do a literal in-tree, in-place `git checkout` of the first commit
    /// 3. Collect public API
    /// 4. Do a literal in-tree, in-place `git checkout` of the second commit
    /// 5. Collect public API
    /// 6. Print the diff between public API in step 2 and step 4
    /// 7. Restore the original branch/commit
    ///
    /// If you have local changes, git will refuse to do `git checkout`, so your work will not be discarded.
    /// To force `git checkout`, use `--force`.
    ///
    /// Using the current git repo has the benefit of making it likely for the build to succeed. If we e.g.
    /// were to git clone a temporary copy of a commit ourselves, the risk is high that additional steps are
    /// needed before a build can succeed. Such as the need to set up git submodules.
    #[clap(verbatim_doc_comment)]
    Diff(DiffArgs),

    /// Generate completion scripts for many different shells.
    ///
    /// Example on how to generate and install the completion script for zsh:
    ///
    ///    $ mkdir ~/.zfunc
    ///    $ rustup completions zsh cargo > ~/.zfunc/_cargo
    ///    $ cargo public-api completions zsh > ~/.zfunc/_cargo-public-api
    ///    $ fpath+=~/.zfunc
    ///    $ autoload -U compinit && compinit
    ///    $ cargo public-api --{{Tab}}
    ///    --all-features         -- Activate all available features
    ///    --cap-lints            -- Forwarded to rustdoc JSON build command
    ///    --color                -- When to color the output
    ///    --debug-sorting        -- Show the hidden "sorting prefix" that makes items nicely grouped
    ///    [...]
    #[clap(verbatim_doc_comment)]
    Completions {
        #[arg(value_enum)]
        shell: clap_complete_command::Shell,
    },
}

enum MainTask {
    /// Print the public API of a crate.
    PrintList {
        api: Box<dyn ApiSource>,
    },
    /// Diff the public API of a crate.
    PrintDiff {
        old_api: Box<dyn ApiSource>,
        new_api: Box<dyn ApiSource>,
    },
    GenerateShellCompletionScript(clap_complete_command::Shell),
}

/// This represents an action that we want to do at some point.
pub enum Action {
    /// The `--deny` arg allows the user to disallow the occurrence of API
    /// changes. We are to check that the diff is allowed.
    CheckDiff {
        diff: PublicApiDiff,
        deny: Vec<DenyMethod>,
    },

    /// Doing a `--diff-git-checkouts` involves doing `git checkout`s.
    /// Afterwards, we want to restore the original branch the user was on, to
    /// not mess up their work tree.
    RestoreBranch { name: String },
}

/// The string used by users to request a diff of the latest (in semver terms)
/// published version of a given crate.
const LATEST_VERSION_ARG: &str = "latest";

fn main_() -> Result<()> {
    // We use the same underlying tracing library as the Rust compiler. Run with
    // the env var `RUST_LOG` set to e.g. `debug` to get started.
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .with_writer(stderr) // See https://github.com/tokio-rs/tracing/issues/2492
        .init();

    let argst = get_args();

    // A list of actions to perform after we have listed or diffed. Typical
    // examples: restore a git branch or check that a diff is allowed
    let mut final_actions = vec![];

    // Now figure out our main task. Typically listing or diffing the public API
    let main_task = main_task(&argst.args)?;

    // If the task we are going to do shortly involves checking out a different
    // commit, set up a restoration of the current branch
    if main_task.changes_commit() {
        final_actions.push(Action::RestoreBranch {
            name: current_branch_or_commit(argst.args.git_root()?)?,
        });
    }

    // Now we perform the main task
    let result = match main_task {
        MainTask::PrintList { api } => print_public_items(&argst, api.as_ref()),
        MainTask::PrintDiff { old_api, new_api } => print_diff(
            &argst,
            old_api.as_ref(),
            new_api.as_ref(),
            &mut final_actions,
        ),
        MainTask::GenerateShellCompletionScript(shell) => {
            shell.generate(
                &mut Args::command().bin_name("cargo-public-api"),
                &mut stdout(),
            );
            Ok(())
        }
    };

    // Handle any final actions, such as checking the diff and restoring the
    // original git branch
    for action in final_actions {
        action.perform(&argst.args)?;
    }

    result
}

fn main_task(args: &Args) -> Result<MainTask> {
    match &args.subcommand {
        Some(Subcommand::Diff(diff_args)) => main_task_from_diff_args(args, diff_args),
        Some(Subcommand::Completions { shell }) => {
            Ok(MainTask::GenerateShellCompletionScript(*shell))
        }
        None => Ok(main_task_from_args(args)),
    }
}

fn main_task_from_args(args: &Args) -> MainTask {
    if let Some(rustdoc_json) = &args.rustdoc_json {
        MainTask::print_list(RustdocJson::new(rustdoc_json.into()).boxed())
    } else {
        MainTask::print_list(CurrentDir.boxed())
    }
}

fn arg_to_api_source(arg: Option<&str>) -> Result<Box<dyn ApiSource>> {
    match arg {
        Some(arg) if is_json_file(arg) => Ok(RustdocJson::new(arg.into()).boxed()),
        Some(arg) if semver::Version::parse(arg).is_ok() => {
            Ok(PublishedCrate::new(Some(arg)).boxed())
        }
        None => Ok(PublishedCrate::new(None).boxed()),
        _ => bail!("Use `ref1..ref2` syntax to diff git commits"),
    }
}

fn main_task_from_diff_args(args: &Args, diff_args: &DiffArgs) -> Result<MainTask> {
    if diff_args.args.len() > 2 {
        bail!(
            "Expected 1 or 2 arguments, but got {}",
            diff_args.args.len()
        )
    }

    let first_arg = diff_args.args.first();
    let second_arg = diff_args.args.get(1);

    let main_task = match (first_arg, second_arg) {
        (Some(first), None) if first.contains("...") => {
            bail!("Invalid git diff syntax: {first}. Use: rev1..rev2");
        }
        (Some(first), None) if first.contains("..") => {
            let commits: Vec<_> = first.split("..").collect();
            if commits.len() != 2 {
                bail!("Expected 2 commits, but got {}", commits.len());
            }
            MainTask::print_diff(
                Commit::new(args, commits[0])?.boxed(),
                Commit::new(args, commits[1])?.boxed(),
            )
        }
        (Some(first), None)
            if semver::Version::parse(first).is_ok() || first == LATEST_VERSION_ARG =>
        {
            MainTask::print_diff(PublishedCrate::new(Some(first)).boxed(), CurrentDir.boxed())
        }
        (Some(first), Some(second)) => MainTask::print_diff(
            arg_to_api_source(Some(first))?,
            arg_to_api_source(Some(second))?,
        ),
        (None, _) => MainTask::print_diff(PublishedCrate::new(None).boxed(), CurrentDir.boxed()),
        (Some(first), None) => {
            bail!("Invalid published crate version syntax: {first}");
        }
    };

    Ok(main_task)
}

/// We were requested to deny diffs, so make sure there is no diff
fn check_diff(deny: &[DenyMethod], diff: &PublicApiDiff) -> Result<()> {
    let mut violations = crate::error::Violations::new();
    for d in deny {
        if d.deny_added() && !diff.added.is_empty() {
            violations.extend_added(diff.added.iter().cloned());
        }
        if d.deny_changed() && !diff.changed.is_empty() {
            violations.extend_changed(diff.changed.iter().cloned());
        }
        if d.deny_removed() && !diff.removed.is_empty() {
            violations.extend_removed(diff.removed.iter().cloned());
        }
    }

    if violations.is_empty() {
        Ok(())
    } else {
        Err(anyhow!(error::Error::DiffDenied(violations)))
    }
}

fn print_public_items(argst: &ArgsAndToolchain, public_api: &dyn ApiSource) -> Result<()> {
    Plain::print_items(
        &mut stdout(),
        &argst.args,
        public_api.obtain_api(argst)?.items(),
    )?;

    Ok(())
}

fn print_diff(
    argst: &ArgsAndToolchain,
    old: &dyn ApiSource,
    new: &dyn ApiSource,
    final_actions: &mut Vec<Action>,
) -> Result<()> {
    fn check_diff(deny: &[DenyMethod], diff: PublicApiDiff) -> Action {
        Action::CheckDiff {
            diff,
            deny: deny.to_owned(),
        }
    }

    let old = old.obtain_api(argst)?;
    let new = new.obtain_api(argst)?;
    let diff = PublicApiDiff::between(old, new);

    Plain::print_diff(&mut stdout(), &argst.args, &diff)?;

    if let Some(Some(deny)) = argst.args.diff_args().map(|a| &a.deny) {
        final_actions.push(check_diff(deny, diff));
    }

    Ok(())
}

impl MainTask {
    fn print_list(api: Box<dyn ApiSource>) -> MainTask {
        Self::PrintList { api }
    }

    fn print_diff(old_api: Box<dyn ApiSource>, new_api: Box<dyn ApiSource>) -> Self {
        Self::PrintDiff { old_api, new_api }
    }

    fn changes_commit(&self) -> bool {
        match self {
            MainTask::PrintDiff { old_api, new_api } => {
                old_api.changes_commit() || new_api.changes_commit()
            }
            MainTask::PrintList { api } => api.changes_commit(),
            MainTask::GenerateShellCompletionScript(_) => false,
        }
    }
}

impl Action {
    fn perform(&self, args: &Args) -> Result<()> {
        match self {
            Action::CheckDiff { deny, diff } => {
                check_diff(deny, diff)?;
            }
            Action::RestoreBranch { name } => {
                git_checkout(args, name)?;
            }
        };
        Ok(())
    }
}

impl Args {
    fn omit_blanket_impls(&self) -> bool {
        self.omits(Omit::BlanketImpls)
    }

    fn omit_auto_trait_impls(&self) -> bool {
        self.omits(Omit::AutoTraitImpls)
    }

    fn omit_auto_derived_impls(&self) -> bool {
        self.omits(Omit::AutoDerivedImpls)
    }

    fn omits(&self, to_omit: Omit) -> bool {
        self.omit.iter().flatten().any(|o| *o == to_omit)
    }

    fn include_function_parameter_names(&self) -> bool {
        self.includes(Include::FunctionParameterNames)
    }

    fn includes(&self, to_include: Include) -> bool {
        self.include.iter().flatten().any(|i| *i == to_include)
    }

    fn git_root(&self) -> Result<PathBuf> {
        git_utils::git_root_from_manifest_path(self.manifest_path.as_path())
    }

    fn diff_args(&self) -> Option<&DiffArgs> {
        match &self.subcommand {
            Some(Subcommand::Diff(diff_args)) => Some(diff_args),
            _ => None,
        }
    }
}

/// Get CLI args via `clap` while also handling when we are invoked as a cargo
/// subcommand. When the user runs `cargo public-api -a -b -c` our args will be
/// `cargo-public-api public-api -a -b -c`.
///
/// Note that we also want to support the binary being installed with a
/// non-standard name such as `~/.cargo/bin/cargo-public-api-v0.13.0`. So we
/// can't assume the bin name is `cargo-public-api`.
fn get_args() -> ArgsAndToolchain {
    let subcommand_name = subcommand_name(std::env::args_os().next().unwrap());
    let args_os = std::env::args_os()
        .enumerate()
        .filter(|(index, arg)| *index != 1 || Some(arg) != subcommand_name.as_ref())
        .map(|(_, arg)| arg);

    let mut args = Args::parse_from(args_os);
    resolve_simplified(&mut args);
    resolve_verbose(&mut args);
    resolve_toolchain(args)
}

/// Strips the `cargo-` prefix from the bin name as well as any extension. For
/// example, `cargo-public-api` becomes `public-api` and
/// `some/path/cargo-public-api-renamed.exe` becomes `public-api-renamed`.
fn subcommand_name(bin: OsString) -> Option<OsString> {
    Some(
        PathBuf::from(bin)
            .file_name()?
            .to_owned()
            .to_string_lossy()
            .strip_prefix("cargo-")?
            .strip_suffix(std::env::consts::EXE_SUFFIX)?
            .to_owned()
            .into(),
    )
}

/// Check if using a stable compiler, and use nightly if it is.
fn resolve_toolchain(args: Args) -> ArgsAndToolchain {
    let toolchain = if toolchain::is_probably_stable() {
        if let Some(toolchain) = toolchain::from_rustup() {
            eprintln!(
                "Warning: using the `{toolchain}` toolchain for gathering the public api is not possible, switching to `nightly`"
            );
        }
        Some("nightly".to_owned())
    } else {
        None
    };

    ArgsAndToolchain { args, toolchain }
}

/// Translates `--simplified` into `--omit` args.
///
/// | number of `--simplified` args | corresponds to `--omit` of                          |
/// |-------------------------------|-----------------------------------------------------|
/// | 1                             | `blanket-impls`                                     |
/// | 2                             | `blanket-impls,auto-trait-impls`                    |
/// | 3                             | `blanket-impls,auto-trait-impls,auto-derived-impls` |
fn resolve_simplified(args: &mut Args) {
    if args.simplified > 0 {
        let omit = args.omit.get_or_insert_with(Vec::new);
        omit.push(Omit::BlanketImpls);

        if args.simplified > 1 {
            omit.push(Omit::AutoTraitImpls);
        }

        if args.simplified > 2 {
            omit.push(Omit::AutoDerivedImpls);
        }
    }
}

fn resolve_verbose(args: &mut Args) {
    if args.verbose > 0 {
        let include = args.include.get_or_insert_with(Vec::new);

        if args.verbose > 0 {
            include.push(Include::FunctionParameterNames);
        }
    }
}

fn is_json_file(file_name: impl AsRef<str>) -> bool {
    Path::extension(Path::new(file_name.as_ref())).is_some_and(|a| a.eq_ignore_ascii_case("json"))
}

/// Helper to reduce code duplication. We can't add [`Args`] to
/// [`git_utils::git_checkout()`] itself, because it is used in contexts where
/// [`Args`] is not available (namely in tests).
fn git_checkout(args: &Args, commit: &str) -> Result<()> {
    git_utils::git_checkout(
        &args.git_root()?,
        commit,
        !args.debug_processing,
        args.diff_args()
            .map(|diff_args| diff_args.force)
            .unwrap_or_default(),
    )
}

/// Wrapper to handle <https://github.com/rust-lang/rust/issues/46016>
fn main() -> Result<()> {
    match main_() {
        Err(e) => match e.root_cause().downcast_ref::<std::io::Error>() {
            Some(io_error) if io_error.kind() == std::io::ErrorKind::BrokenPipe => {
                std::process::exit(141)
            }
            _ => Err(e),
        },
        result => result,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn verify_cli() {
        use clap::CommandFactory;
        Args::command().debug_assert();
    }

    #[test]
    fn test_subcommand_name() {
        for test in [
            ("cargo-public-api", Some("public-api")),
            ("cargo-public-api-v0.13.0", Some("public-api-v0.13.0")),
            ("relative/path/cargo-public-api", Some("public-api")),
            ("relative/cargo-public-api.foo", Some("public-api.foo")),
            ("cargo-public-api-secondary", Some("public-api-secondary")),
            ("cargo-something-else", Some("something-else")),
            ("prefix-cargo-public-api", None),
            ("/some/abs/path/cargo-public-api", Some("public-api")),
            #[cfg(windows)]
            ("c:\\abs\\cargo-public-api-old", Some("public-api-old")),
        ] {
            assert_eq!(
                subcommand_name(OsString::from(&format!(
                    "{}{}",
                    test.0,
                    std::env::consts::EXE_SUFFIX
                ))),
                test.1.map(OsString::from),
                "failed to parse \"{}\"",
                test.0,
            );
        }
    }
}