Skip to content

Helpful error for invalid cargo add #15688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

kornelski
Copy link
Contributor

When users try to use paths and URLs directly in cargo add, like cargo add ../path or cargo add ssh://git/dep.git, cargo treats it as a package name syntax error and complains that "characters must be Unicode XID characters", which isn't addressing users' misconception.

I've added "note: ..." with recovery suggestions to these errors.

I've also noticed that cargo add --path dir/Cargo.toml errs with "failed to read dir/Cargo.toml/Cargo.toml". Explaining that failure case felt silly, so I've made --path automatically strip Cargo.toml from the path instead.

@rustbot
Copy link
Collaborator

rustbot commented Jun 21, 2025

r? @ehuss

rustbot has assigned @ehuss.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added Command-add S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 21, 2025
Copy link
Contributor

@epage epage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the errors better!

let path = paths::normalize_path(&std::env::current_dir()?.join(raw_path));
let mut relative_path = Path::new(raw_path);
if relative_path.ends_with("Cargo.toml") {
relative_path = relative_path.parent().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd like to speed up this PR, I'd recommend removing this as it is a new feature that adds to our compatibility surface that mirrors other features,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just changed it to an error. I feel that if I make it a separate PR, you'll ask to make it consistent with install, and install uses SourceId here, and that would require a whole refactoring of both commands, and that's too much for me.

@@ -104,6 +105,14 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
options.gctx,
&mut registry,
)
.map_err(|err| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for this change. Ideally, add it in a commit before, with it passing so it shows the current behavior and this commit would then show how the behavior changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle I agree this should be tested, but the test framework has such high overhead that I think it's unreasonable to use it for such a tiny feature: #15691

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While there might be overhead, we'd still want to see tests for changes, even ones like this. While there can be improvements, how we currently test is the reality we are currently in and doesn't affect the need for tests.

For example, a test will catch if the error you are matching on changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've restructured the code to require an exact error type (so that the integration point is checked by the type system, not a test). Then I've added a localized unit test for the suggestion. I think this way it can test more cases, quicker, and isn't as sensitive to irrelevant UI changes as the end-to-end tests are.

{
Some("git URLs must be specified with --git")
} else if arg.registry.is_none()
&& (spec.starts_with("registry+") || spec.starts_with("sparse+"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--registry values do not start with those but are names to look up urls in config

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the message

fn spec_fix_suggestion(arg: &DepOp) -> Option<&'static str> {
let spec = arg.crate_spec.as_deref()?;

if arg.git.is_none()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each of these, you are checking if a related flag is used. How come?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to avoid suggesting a flag that has already been specified by the user

{
Some("registy can be specified with --registry")
} else if spec.contains("://") {
Some("`cargo add` expects crates specified as 'name' or 'name@version', not URLs")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be a git url as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's why the text is phrased to fit use along --git too (e.g. in case they swapped url with crate names)

@epage
Copy link
Contributor

epage commented Jun 21, 2025

Should we extend this to cargo install which has a similar cli?

@@ -378,7 +408,11 @@ fn resolve_dependency(
};
selected
} else if let Some(raw_path) = &arg.path {
let path = paths::normalize_path(&std::env::current_dir()?.join(raw_path));
let relative_path = Path::new(raw_path);
if relative_path.ends_with("Cargo.toml") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is a directory named Cargo.toml or we change what we allow for paths?

This is why I like the other approach of augmenting an existing error rather than assuming its an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get your concern about edge cases, and I usually prefer to implement a robust solution. However, these edge cases are extremely unlikely to cause any real problems, and I don't think any other solution here is practical. From the perspective of what I can hope to achieve in Cargo, it's either this or nothing.

The problem is that the codebase doesn't already have a proper place to do this where this would count as a small-enough non-objectionable change.

The conversion from path CLI arguments to path sources is already done differently in multiple places. Duplicating the same hack in multiple places wouldn't be appropriate, but not duplicating the hack would require refactoring the code to remove the duplication first. I would actually like to do that! I enjoy doing such code cleanups!

However, I know you don't approve of unsolicited refactors like that, and you don't trust my judgement on program's architecture or coding style. The process for proposing, justifying, and getting approval for a cross-cutting refactoring in Cargo is prohibitively time-consuming. For an outside contributor like me it adds non-coding overhead that is totally out of proportion of the problem being solved here.

So I'm leaving the crappy error:

error: failed to read …/Cargo.toml/Cargo.toml

} else if arg.registry.is_none()
&& (spec.starts_with("registry+") || spec.starts_with("sparse+"))
{
Some("registy can be specified with --registry name")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is a bit ambiguous. What about making its association and being a placeholder more explicit

registry can be specified with `--registry <name>`

Should we do similar for the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've used convention from the --help, with <NAME>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Command-add S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants