diff --git a/docs/src/checks/bans/cfg.md b/docs/src/checks/bans/cfg.md index bec8301a..93ed17d8 100644 --- a/docs/src/checks/bans/cfg.md +++ b/docs/src/checks/bans/cfg.md @@ -28,9 +28,9 @@ Determines what happens when a dependency is specified with the `*` (wildcard) v If specified, alters how the `wildcard` field behaves: - * [path](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-path-dependencies) `dependencies` in **private** crates will no longer emit a warning or error. - * path `dev-dependencies` in both public and private crates will no longer emit a warning or error. - * path `dependencies` and `build-dependencies` in **public** crates will continue to produce warnings and errors. +* [path](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-path-dependencies) `dependencies` in **private** crates will no longer emit a warning or error. +* path `dev-dependencies` in both public and private crates will no longer emit a warning or error. +* path `dependencies` and `build-dependencies` in **public** crates will continue to produce warnings and errors. Being limited to private crates is due to crates.io not allowing packages to be published with `path` dependencies except for `dev-dependencies`. @@ -76,6 +76,15 @@ deny = [{ name = "crate-you-don't-want", version = "<= 0.7.0", wrappers = ["this This field allows specific crates to have a direct dependency on the banned crate but denies all transitive dependencies on it. +#### The `deny-multiple-versions` field (optional) + +```ini +multiple-versions = 'allow' +deny = [{ name = "crate-you-want-only-one-version-of", deny-multiple-versions = true }] +``` + +This field allows specific crates to deny multiple versions of themselves, but allowing or warning on multiple versions for all other crates. This field cannot be set simultaneously with `wrappers`. + ### The `allow` field (optional) Determines specific crates that are allowed. If the `allow` list has one or more entries, then any crate not in that list will be denied, so use with care. diff --git a/src/bans.rs b/src/bans.rs index b49d6fb1..ea03fe2d 100644 --- a/src/bans.rs +++ b/src/bans.rs @@ -193,6 +193,7 @@ pub fn check( let ValidConfig { file_id, denied, + denied_multiple_versions, allowed, features, workspace_default_features, @@ -236,14 +237,23 @@ pub fn check( }; let report_duplicates = |multi_detector: &MultiDetector<'_>, sink: &mut diag::ErrorSink| { - if multi_detector.dupes.len() <= 1 || multiple_versions == LintLevel::Allow { + if multi_detector.dupes.len() <= 1 { return; } - let severity = match multiple_versions { + let lint_level = if multi_detector.dupes.iter().any(|kindex| { + let krate = &ctx.krates[*kindex]; + matches(&denied_multiple_versions, krate).is_some() + }) { + LintLevel::Deny + } else { + multiple_versions + }; + + let severity = match lint_level { LintLevel::Warn => Severity::Warning, LintLevel::Deny => Severity::Error, - LintLevel::Allow => unreachable!(), + LintLevel::Allow => return, }; let mut all_start = std::usize::MAX; diff --git a/src/bans/cfg.rs b/src/bans/cfg.rs index 6ac366b8..b003f3ac 100644 --- a/src/bans/cfg.rs +++ b/src/bans/cfg.rs @@ -24,7 +24,10 @@ pub struct CrateBan { pub version: Option, /// One or more crates that will allow this crate to be used if it is a /// direct dependency - pub wrappers: Option>>, + pub wrappers: Option>>>, + /// Setting this to true will only emit an error if multiple + // versions of the crate are found + pub deny_multiple_versions: Option>, } #[derive(Deserialize, Clone)] @@ -163,8 +166,14 @@ impl crate::cfg::UnvalidatedConfig for Config { ) }; - let denied: Vec<_> = self - .deny + let (deny_multiple_versions, deny): (Vec<_>, Vec<_>) = + self.deny.into_iter().partition(|kb| { + kb.deny_multiple_versions + .as_ref() + .map_or(false, |spanned| spanned.value) + }); + + let denied: Vec<_> = deny .into_iter() .map(|cb| KrateBan { id: Skrate::new( @@ -174,7 +183,39 @@ impl crate::cfg::UnvalidatedConfig for Config { }, cb.name.span, ), - wrappers: cb.wrappers, + wrappers: cb.wrappers.map(|spanned| spanned.value), + }) + .collect(); + + let denied_multiple_versions: Vec<_> = deny_multiple_versions + .into_iter() + .map(|cb| { + let wrappers = cb.wrappers.filter(|spanned| !spanned.value.is_empty()); + if let Some(wrappers) = wrappers { + // cb.multiple_versions is guaranteed to be Some(_) by the + // earlier call to `partition` + let multiple_versions = cb.deny_multiple_versions.unwrap(); + diags.push( + Diagnostic::error() + .with_message( + "a crate ban was specified with both `wrappers` and `multiple-versions`", + ) + .with_labels(vec![ + Label::secondary(cfg_file, wrappers.span) + .with_message("has one or more `wrappers`"), + Label::secondary(cfg_file, multiple_versions.span) + .with_message("has `multiple-versions` set to true"), + ]), + ); + } + + Skrate::new( + KrateId { + name: cb.name.value, + version: cb.version, + }, + cb.name.span, + ) }) .collect(); @@ -260,6 +301,7 @@ impl crate::cfg::UnvalidatedConfig for Config { multiple_versions: self.multiple_versions, highlight: self.highlight, denied, + denied_multiple_versions, allowed, features, external_default_features: self.external_default_features, @@ -319,6 +361,7 @@ pub struct ValidConfig { pub multiple_versions: LintLevel, pub highlight: GraphHighlight, pub(crate) denied: Vec, + pub(crate) denied_multiple_versions: Vec, pub(crate) allowed: Vec, pub(crate) features: Vec, pub external_default_features: Option>, diff --git a/tests/bans.rs b/tests/bans.rs index e3a1969b..048fa7ee 100644 --- a/tests/bans.rs +++ b/tests/bans.rs @@ -120,3 +120,22 @@ fn duplicate_graphs() { insta::assert_debug_snapshot!(dup_graphs.lock().unwrap()); } + +/// Ensures that we can allow duplicates generally, but deny them for specific +/// crates +#[test] +fn deny_multiple_versions_for_specific_krates() { + let diags = gather_bans( + func_name!(), + KrateGather::new("duplicates"), + r#" +multiple-versions = 'allow' +deny = [ + { name = 'block-buffer', deny-multiple-versions = true }, + { name = 'generic-array', deny-multiple-versions = true }, +] +"#, + ); + + insta::assert_json_snapshot!(diags); +} diff --git a/tests/snapshots/bans__deny_multiple_versions_for_specific_krates.snap b/tests/snapshots/bans__deny_multiple_versions_for_specific_krates.snap new file mode 100644 index 00000000..c4262d3c --- /dev/null +++ b/tests/snapshots/bans__deny_multiple_versions_for_specific_krates.snap @@ -0,0 +1,380 @@ +--- +source: tests/bans.rs +expression: diags +--- +[ + { + "fields": { + "code": "duplicate", + "graphs": [ + { + "Krate": { + "name": "block-buffer", + "version": "0.7.3" + }, + "parents": [ + { + "Krate": { + "name": "sha-1", + "version": "0.8.2" + }, + "parents": [ + { + "Krate": { + "kind": "build", + "name": "pest_meta", + "version": "2.1.3" + }, + "parents": [ + { + "Krate": { + "name": "pest_generator", + "version": "2.1.3" + }, + "parents": [ + { + "Krate": { + "name": "pest_derive", + "version": "2.1.0" + }, + "parents": [ + { + "Krate": { + "name": "async-graphql-parser", + "version": "3.0.38" + }, + "parents": [ + { + "Krate": { + "name": "async-graphql", + "version": "3.0.38" + }, + "parents": [ + { + "Krate": { + "name": "duplicates", + "version": "0.1.0" + } + } + ] + }, + { + "Krate": { + "name": "async-graphql-derive", + "version": "3.0.38" + }, + "parents": [ + { + "Krate": { + "name": "async-graphql", + "version": "3.0.38" + }, + "repeat": true + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "Krate": { + "name": "block-buffer", + "version": "0.10.2" + }, + "parents": [ + { + "Krate": { + "name": "digest", + "version": "0.10.3" + }, + "parents": [ + { + "Krate": { + "name": "sha2", + "version": "0.10.2" + }, + "parents": [ + { + "Krate": { + "name": "sqlx-core", + "version": "0.5.13" + }, + "parents": [ + { + "Krate": { + "name": "sqlx", + "version": "0.5.13" + }, + "parents": [ + { + "Krate": { + "name": "duplicates", + "version": "0.1.0" + } + } + ] + }, + { + "Krate": { + "name": "sqlx-macros", + "version": "0.5.13" + }, + "parents": [ + { + "Krate": { + "name": "sqlx", + "version": "0.5.13" + }, + "repeat": true + } + ] + } + ] + }, + { + "Krate": { + "name": "sqlx-macros", + "version": "0.5.13" + }, + "repeat": true + } + ] + } + ] + } + ] + } + ], + "labels": [ + { + "column": 1, + "line": 15, + "message": "lock entries", + "span": "block-buffer 0.7.3 registry+https://github.com/rust-lang/crates.io-index\nblock-buffer 0.10.2 registry+https://github.com/rust-lang/crates.io-index" + } + ], + "message": "found 2 duplicate entries for crate 'block-buffer'", + "severity": "error" + }, + "type": "diagnostic" + }, + { + "fields": { + "code": "duplicate", + "graphs": [ + { + "Krate": { + "name": "generic-array", + "version": "0.12.4" + }, + "parents": [ + { + "Krate": { + "name": "block-buffer", + "version": "0.7.3" + }, + "parents": [ + { + "Krate": { + "name": "sha-1", + "version": "0.8.2" + }, + "parents": [ + { + "Krate": { + "kind": "build", + "name": "pest_meta", + "version": "2.1.3" + }, + "parents": [ + { + "Krate": { + "name": "pest_generator", + "version": "2.1.3" + }, + "parents": [ + { + "Krate": { + "name": "pest_derive", + "version": "2.1.0" + }, + "parents": [ + { + "Krate": { + "name": "async-graphql-parser", + "version": "3.0.38" + }, + "parents": [ + { + "Krate": { + "name": "async-graphql", + "version": "3.0.38" + }, + "parents": [ + { + "Krate": { + "name": "duplicates", + "version": "0.1.0" + } + } + ] + }, + { + "Krate": { + "name": "async-graphql-derive", + "version": "3.0.38" + }, + "parents": [ + { + "Krate": { + "name": "async-graphql", + "version": "3.0.38" + }, + "repeat": true + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "Krate": { + "name": "digest", + "version": "0.8.1" + }, + "parents": [ + { + "Krate": { + "name": "sha-1", + "version": "0.8.2" + }, + "repeat": true + } + ] + } + ] + }, + { + "Krate": { + "name": "generic-array", + "version": "0.14.5" + }, + "parents": [ + { + "Krate": { + "name": "block-buffer", + "version": "0.10.2" + }, + "parents": [ + { + "Krate": { + "name": "digest", + "version": "0.10.3" + }, + "parents": [ + { + "Krate": { + "name": "sha2", + "version": "0.10.2" + }, + "parents": [ + { + "Krate": { + "name": "sqlx-core", + "version": "0.5.13" + }, + "parents": [ + { + "Krate": { + "name": "sqlx", + "version": "0.5.13" + }, + "parents": [ + { + "Krate": { + "name": "duplicates", + "version": "0.1.0" + } + } + ] + }, + { + "Krate": { + "name": "sqlx-macros", + "version": "0.5.13" + }, + "parents": [ + { + "Krate": { + "name": "sqlx", + "version": "0.5.13" + }, + "repeat": true + } + ] + } + ] + }, + { + "Krate": { + "name": "sqlx-macros", + "version": "0.5.13" + }, + "repeat": true + } + ] + } + ] + } + ] + }, + { + "Krate": { + "name": "crypto-common", + "version": "0.1.3" + }, + "parents": [ + { + "Krate": { + "name": "digest", + "version": "0.10.3" + }, + "repeat": true + } + ] + } + ] + } + ], + "labels": [ + { + "column": 1, + "line": 50, + "message": "lock entries", + "span": "generic-array 0.12.4 registry+https://github.com/rust-lang/crates.io-index\ngeneric-array 0.14.5 registry+https://github.com/rust-lang/crates.io-index" + } + ], + "message": "found 2 duplicate entries for crate 'generic-array'", + "severity": "error" + }, + "type": "diagnostic" + } +]