Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::lints::rules::non_snake_case_features;
use crate::lints::rules::non_snake_case_packages;
use crate::lints::rules::redundant_homepage;
use crate::lints::rules::redundant_readme;
use crate::lints::rules::uninherited_repository;
use crate::lints::rules::unused_workspace_dependencies;
use crate::lints::rules::unused_workspace_package_fields;
use crate::ops;
Expand Down Expand Up @@ -1391,6 +1392,14 @@ impl<'gctx> Workspace<'gctx> {
non_snake_case_features(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?;
redundant_readme(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?;
redundant_homepage(pkg, &path, &cargo_lints, &mut run_error_count, self.gctx)?;
uninherited_repository(
self,
pkg,
&path,
&cargo_lints,
&mut run_error_count,
self.gctx,
)?;
missing_lints_inheritance(
self,
pkg,
Expand Down
3 changes: 3 additions & 0 deletions src/cargo/lints/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod non_snake_case_features;
mod non_snake_case_packages;
mod redundant_homepage;
mod redundant_readme;
mod uninherited_repository;
mod unknown_lints;
mod unused_workspace_dependencies;
mod unused_workspace_package_fields;
Expand All @@ -25,6 +26,7 @@ pub use non_snake_case_features::non_snake_case_features;
pub use non_snake_case_packages::non_snake_case_packages;
pub use redundant_homepage::redundant_homepage;
pub use redundant_readme::redundant_readme;
pub use uninherited_repository::uninherited_repository;
pub use unknown_lints::output_unknown_lints;
pub use unused_workspace_dependencies::unused_workspace_dependencies;
pub use unused_workspace_package_fields::unused_workspace_package_fields;
Expand All @@ -41,6 +43,7 @@ pub static LINTS: &[&crate::lints::Lint] = &[
non_snake_case_packages::LINT,
redundant_homepage::LINT,
redundant_readme::LINT,
uninherited_repository::LINT,
unknown_lints::LINT,
unused_workspace_dependencies::LINT,
unused_workspace_package_fields::LINT,
Expand Down
183 changes: 183 additions & 0 deletions src/cargo/lints/rules/uninherited_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use std::path::Path;

use annotate_snippets::AnnotationKind;
use annotate_snippets::Group;
use annotate_snippets::Level;
use annotate_snippets::Origin;
use annotate_snippets::Patch;
use annotate_snippets::Snippet;
use cargo_util_schemas::manifest::InheritableField;
use cargo_util_schemas::manifest::TomlToolLints;

use crate::CargoResult;
use crate::GlobalContext;
use crate::core::Package;
use crate::core::Workspace;
use crate::lints::Lint;
use crate::lints::LintLevel;
use crate::lints::LintLevelReason;
use crate::lints::PEDANTIC;
use crate::lints::get_key_value_span;
use crate::lints::rel_cwd_manifest_path;

pub static LINT: &Lint = &Lint {
name: "uninherited_repository",
desc: "`package.repository` in a workspace member should be inherited from `[workspace.package]`",
primary_group: &PEDANTIC,
msrv: Some(super::CARGO_LINTS_MSRV),
edition_lint_opts: None,
feature_gate: None,
docs: Some(
r#"
### What it does

Checks for `package.repository` fields in workspace members that are set
explicitly instead of being inherited from `[workspace.package]`.

See also [`package.repository` reference documentation](manifest.md#the-repository-field).

### Why it is bad

A common mistake is setting `package.repository` to the URL of the git host's
file browser for the specific crate (e.g. a GitHub `/tree/` URL) rather than
the root of the repository. Moving the field to `[workspace.package]` and
inheriting it encourages using the correct root URL in one place and avoids
per-crate drift.

### Note

This lint fires for any explicit `repository` value in a workspace member,
regardless of whether the URL looks correct or not. It does not validate the
URL itself; it only checks that the field is inherited rather than set
explicitly.

### Drawbacks

A workspace that spans multiple repositories would need to suppress this lint,
since each member legitimately has a different repository URL.

### Example

```toml
[workspace]

[package]
repository = "https://github.com/rust-lang/cargo/tree/master/crates/cargo-test-macro"
```

Should be written as:

```toml
[workspace.package]
repository = "https://github.com/rust-lang/cargo"

[package]
repository.workspace = true
```
"#,
),
};

pub fn uninherited_repository(
ws: &Workspace<'_>,
pkg: &Package,
manifest_path: &Path,
cargo_lints: &TomlToolLints,
error_count: &mut usize,
gctx: &GlobalContext,
) -> CargoResult<()> {
let (lint_level, reason) = LINT.level(
cargo_lints,
pkg.rust_version(),
pkg.manifest().edition(),
pkg.manifest().unstable_features(),
);

if lint_level == LintLevel::Allow {
return Ok(());
}

// Use normalized_toml here: we only need to know whether a [workspace]
// section exists at all.
let has_workspace = ws.root_maybe().normalized_toml().workspace.is_some();

if !has_workspace {
return Ok(());
}

let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);

lint_package(pkg, &manifest_path, lint_level, reason, error_count, gctx)
}

pub fn lint_package(
pkg: &Package,
manifest_path: &str,
lint_level: LintLevel,
reason: LintLevelReason,
error_count: &mut usize,
gctx: &GlobalContext,
) -> CargoResult<()> {
let manifest = pkg.manifest();

// Use original_toml so we see the raw InheritableField before workspace
// inheritance has been resolved into a plain Value.
let Some(original_toml) = manifest.original_toml() else {
return Ok(());
};
let Some(original_pkg) = &original_toml.package else {
return Ok(());
};
let Some(repository) = &original_pkg.repository else {
return Ok(());
};

let InheritableField::Value(_) = repository else {
// Already inheriting from workspace so nothing to do.
return Ok(());
};

let document = manifest.document();
let contents = manifest.contents();
let level = lint_level.to_diagnostic_level();
let emitted_source = LINT.emitted_source(lint_level, reason);

let mut primary = Group::with_title(level.primary_title(LINT.desc));
if let Some(document) = document
&& let Some(contents) = contents
&& let Some(span) = get_key_value_span(document, &["package", "repository"])
{
primary = primary.element(
Snippet::source(contents)
.path(manifest_path)
.annotation(AnnotationKind::Primary.span(span.key.start..span.value.end)),
);
} else {
primary = primary.element(Origin::path(manifest_path));
}
primary = primary.element(Level::NOTE.message(emitted_source));
let mut report = vec![primary];

if let Some(document) = document
&& let Some(contents) = contents
&& let Some(span) = get_key_value_span(document, &["package", "repository"])
{
let remove_span = span.key.start..span.value.end;
let mut help = Group::with_title(Level::HELP.secondary_title(
"consider moving `repository` to `[workspace.package]` and inheriting it",
));
help = help.element(
Snippet::source(contents)
.path(manifest_path)
.patch(Patch::new(remove_span, "repository.workspace = true")),
);
report.push(help);
}

if lint_level.is_error() {
*error_count += 1;
}
gctx.shell().print_report(&report, lint_level.force())?;

Ok(())
}
55 changes: 55 additions & 0 deletions src/doc/src/reference/lints.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ These lints are all set to the 'allow' level by default.
- [`non_kebab_case_packages`](#non_kebab_case_packages)
- [`non_snake_case_features`](#non_snake_case_features)
- [`non_snake_case_packages`](#non_snake_case_packages)
- [`uninherited_repository`](#uninherited_repository)

## Warn-by-default

Expand Down Expand Up @@ -393,6 +394,60 @@ name = "foo"
```


## `uninherited_repository`
Group: `pedantic`

Level: `allow`

MSRV: `1.79.0`

### What it does

Checks for `package.repository` fields in workspace members that are set
explicitly instead of being inherited from `[workspace.package]`.

See also [`package.repository` reference documentation](manifest.md#the-repository-field).

### Why it is bad

A common mistake is setting `package.repository` to the URL of the git host's
file browser for the specific crate (e.g. a GitHub `/tree/` URL) rather than
the root of the repository. Moving the field to `[workspace.package]` and
inheriting it encourages using the correct root URL in one place and avoids
per-crate drift.

### Note

This lint fires for any explicit `repository` value in a workspace member,
regardless of whether the URL looks correct or not. It does not validate the
URL itself; it only checks that the field is inherited rather than set
explicitly.

### Drawbacks

A workspace that spans multiple repositories would need to suppress this lint,
since each member legitimately has a different repository URL.

### Example

```toml
[workspace]

[package]
repository = "https://github.com/rust-lang/cargo/tree/master/crates/cargo-test-macro"
```

Should be written as:

```toml
[workspace.package]
repository = "https://github.com/rust-lang/cargo"

[package]
repository.workspace = true
```


## `unknown_lints`
Group: `suspicious`

Expand Down
1 change: 1 addition & 0 deletions tests/testsuite/lints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod non_snake_case_features;
mod non_snake_case_packages;
mod redundant_homepage;
mod redundant_readme;
mod uninherited_repository;
mod unknown_lints;
mod unused_workspace_dependencies;
mod unused_workspace_package_fields;
Expand Down
Loading
Loading