Skip to content

Commit ccbff6f

Browse files
committed
Helpful error for confused cargo add arguments
1 parent 0731d17 commit ccbff6f

File tree

2 files changed

+113
-1
lines changed

2 files changed

+113
-1
lines changed

src/cargo/ops/cargo_add/crate_spec.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
//! Crate name parsing.
22
33
use anyhow::Context as _;
4+
use thiserror::Error;
45

56
use super::Dependency;
67
use crate::util::toml_mut::dependency::RegistrySource;
78
use crate::CargoResult;
89
use cargo_util_schemas::manifest::PackageName;
910

11+
#[derive(Error, Debug)]
12+
#[error(transparent)]
13+
pub(crate) struct CrateSpecResolutionError(pub crate::Error);
14+
1015
/// User-specified crate
1116
///
1217
/// This can be a
@@ -22,7 +27,12 @@ pub struct CrateSpec {
2227

2328
impl CrateSpec {
2429
/// Convert a string to a `Crate`
25-
pub fn resolve(pkg_id: &str) -> CargoResult<Self> {
30+
pub fn resolve(pkg_id: &str) -> Result<Self, CrateSpecResolutionError> {
31+
// enables targeted add_spec_fix_suggestion
32+
Self::resolve_inner(pkg_id).map_err(CrateSpecResolutionError)
33+
}
34+
35+
fn resolve_inner(pkg_id: &str) -> CargoResult<Self> {
2636
let (name, version) = pkg_id
2737
.split_once('@')
2838
.map(|(n, v)| (n, Some(v)))

src/cargo/ops/cargo_add/mod.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use cargo_util::paths;
1414
use cargo_util_schemas::core::PartialVersion;
1515
use cargo_util_schemas::manifest::PathBaseName;
1616
use cargo_util_schemas::manifest::RustVersion;
17+
use crate_spec::CrateSpecResolutionError;
1718
use indexmap::IndexSet;
1819
use itertools::Itertools;
1920
use toml_edit::Item as TomlItem;
@@ -104,6 +105,10 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
104105
options.gctx,
105106
&mut registry,
106107
)
108+
.map_err(|err| match err.downcast::<CrateSpecResolutionError>() {
109+
Ok(err) => add_spec_fix_suggestion(err, raw),
110+
Err(other) => other,
111+
})
107112
})
108113
.collect::<CargoResult<Vec<_>>>()?
109114
};
@@ -1258,3 +1263,100 @@ fn precise_version(version_req: &semver::VersionReq) -> Option<String> {
12581263
.max()
12591264
.map(|v| v.to_string())
12601265
}
1266+
1267+
/// Help with invalid arguments to `cargo add`
1268+
fn add_spec_fix_suggestion(err: CrateSpecResolutionError, arg: &DepOp) -> crate::Error {
1269+
if let Some(note) = spec_fix_suggestion_inner(arg) {
1270+
return anyhow::format_err!("{err}\nnote: {note}");
1271+
}
1272+
err.into()
1273+
}
1274+
1275+
fn spec_fix_suggestion_inner(arg: &DepOp) -> Option<&'static str> {
1276+
let spec = arg.crate_spec.as_deref()?;
1277+
1278+
let looks_like_git_url = spec.starts_with("git@")
1279+
|| spec.starts_with("ssh:")
1280+
|| spec.ends_with(".git")
1281+
|| spec.starts_with("https://git"); // compromise between favoring a couple of top sites vs trying to list every git host
1282+
1283+
// check if the arg is present to avoid suggesting it redundantly
1284+
if arg.git.is_none() && looks_like_git_url {
1285+
Some("git URLs must be specified with --git <URL>")
1286+
} else if arg.registry.is_none()
1287+
&& (spec.starts_with("registry+") || spec.starts_with("sparse+"))
1288+
{
1289+
Some("registy can be specified with --registry <NAME>")
1290+
} else if spec.contains("://") || looks_like_git_url {
1291+
Some("`cargo add` expects crates specified as 'name' or 'name@version', not URLs")
1292+
} else if arg.path.is_none() && spec.contains('/') {
1293+
Some("local crates can be added with --path <DIR>")
1294+
} else {
1295+
None
1296+
}
1297+
}
1298+
1299+
#[test]
1300+
fn test_spec_fix_suggestion() {
1301+
fn dep(crate_spec: &str) -> DepOp {
1302+
DepOp {
1303+
crate_spec: Some(crate_spec.into()),
1304+
rename: None,
1305+
features: None,
1306+
default_features: None,
1307+
optional: None,
1308+
public: None,
1309+
registry: None,
1310+
path: None,
1311+
base: None,
1312+
git: None,
1313+
branch: None,
1314+
rev: None,
1315+
tag: None,
1316+
}
1317+
}
1318+
1319+
#[track_caller]
1320+
fn err_for(dep: &DepOp) -> String {
1321+
add_spec_fix_suggestion(
1322+
CrateSpec::resolve(dep.crate_spec.as_deref().unwrap()).unwrap_err(),
1323+
&dep,
1324+
)
1325+
.to_string()
1326+
}
1327+
1328+
for path in ["../some/path", "/absolute/path", "~/dir/Cargo.toml"] {
1329+
let mut dep = dep(path);
1330+
assert!(err_for(&dep).contains("note: local crates can be added with --path <DIR>"));
1331+
1332+
dep.path = Some(".".into());
1333+
assert!(!err_for(&dep).contains("--git"));
1334+
assert!(!err_for(&dep).contains("--registry"));
1335+
assert!(!err_for(&dep).contains("--path"));
1336+
}
1337+
1338+
assert!(err_for(&dep(
1339+
"registry+https://private.local:8000/index#[email protected]"
1340+
))
1341+
.contains("--registry <NAME>"));
1342+
1343+
for git_url in [
1344+
"git@host:dir/repo.git",
1345+
"https://gitlab.com/~user/crate.git",
1346+
"ssh://host/path",
1347+
] {
1348+
let mut dep = dep(git_url);
1349+
let msg = err_for(&dep);
1350+
assert!(
1351+
msg.contains("note: git URLs must be specified with --git <URL>"),
1352+
"{msg} {dep:?}"
1353+
);
1354+
assert!(!err_for(&dep).contains("--path"));
1355+
assert!(!err_for(&dep).contains("--registry"));
1356+
1357+
dep.git = Some("true".into());
1358+
let msg = err_for(&dep);
1359+
assert!(!msg.contains("--git"));
1360+
assert!(msg.contains("'name@version', not URLs"), "{msg} {dep:?}");
1361+
}
1362+
}

0 commit comments

Comments
 (0)