diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index fcfbc012d62..9f0c0973beb 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -65,13 +65,12 @@ use std::fs::{self, File}; use std::io::{BufRead, BufWriter, Write}; use std::ops::{Deref, Range}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock}; +use std::sync::Arc; use anyhow::{Context as _, Error}; use cargo_platform::{Cfg, Platform}; use cargo_util_terminal::report::{AnnotationKind, Group, Level, Renderer, Snippet}; use itertools::Itertools; -use regex::Regex; use tracing::{debug, instrument, trace}; pub use self::build_config::UserIntent; @@ -106,7 +105,10 @@ use crate::compiler::timings::SectionTiming; pub use crate::compiler::unit::Unit; pub use crate::compiler::unit::UnitIndex; pub use crate::compiler::unit::UnitInterner; -use crate::diagnostics::get_key_value; +use crate::diagnostics::{ + PublicDependencySuggestion, exported_private_dependency_name, get_key_value, + public_dependency_suggestion_from_value, push_public_dependency_suggestion_to_diagnostic, +}; use crate::util::OnceExt; use crate::util::errors::{CargoResult, VerboseError}; use crate::util::interning::InternedString; @@ -2111,28 +2113,17 @@ fn on_stderr_line_inner( // Returns `true` if the diagnostic was modified. let add_pub_in_priv_diagnostic = |diag: &mut String| -> bool { - // We are parsing the compiler diagnostic here, as this information isn't - // currently exposed elsewhere. - // At the time of writing this comment, rustc emits two different - // "exported_private_dependencies" errors: - // - type `FromPriv` from private dependency 'priv_dep' in public interface - // - struct `FromPriv` from private dependency 'priv_dep' is re-exported - // This regex matches them both. To see if it needs to be updated, grep the rust - // source for "EXPORTED_PRIVATE_DEPENDENCIES". - static PRIV_DEP_REGEX: LazyLock = - LazyLock::new(|| Regex::new("from private dependency '([A-Za-z0-9-_]+)'").unwrap()); - if let Some(crate_name) = PRIV_DEP_REGEX.captures(diag).and_then(|m| m.get(1)) + if let Some(crate_name) = exported_private_dependency_name(diag) && let Some(ref contents) = manifest.contents - && let Some(span) = manifest.find_crate_span(crate_name.as_str()) + && let Some(span) = manifest.find_crate_span(crate_name) { let rel_path = pathdiff::diff_paths(&manifest.path, &manifest.cwd) .unwrap_or_else(|| manifest.path.clone()) .display() .to_string(); - let report = [Group::with_title(Level::NOTE.secondary_title(format!( - "dependency `{}` declared here", - crate_name.as_str() - ))) + let report = [Group::with_title( + Level::NOTE.secondary_title(format!("dependency `{}` declared here", crate_name)), + ) .element( Snippet::source(contents) .path(rel_path) @@ -2148,6 +2139,20 @@ fn on_stderr_line_inner( } false }; + let add_pub_in_priv_suggestion = |diag: &mut serde_json::Value| -> bool { + let Some(crate_name) = diag + .get("message") + .and_then(|message| message.as_str()) + .and_then(exported_private_dependency_name) + .map(str::to_owned) + else { + return false; + }; + let Some(suggestion) = manifest.find_crate_public_suggestion(&crate_name) else { + return false; + }; + push_public_dependency_suggestion_to_diagnostic(diag, &crate_name, suggestion) + }; // Depending on what we're emitting from Cargo itself, we figure out what to // do with this JSON message. @@ -2215,7 +2220,7 @@ fn on_stderr_line_inner( } let mut rendered = msg.rendered; if options.show_diagnostics { - let machine_applicable: bool = msg + let rustc_machine_applicable: bool = msg .children .iter() .map(|child| { @@ -2226,6 +2231,17 @@ fn on_stderr_line_inner( .any(|app| app == Applicability::MachineApplicable) }) .any(|b| b); + let manifest_machine_applicable = msg + .code + .as_ref() + .is_some_and(|c| c.code == "exported_private_dependencies") + && exported_private_dependency_name(&msg.message) + .and_then(|crate_name| { + manifest.find_crate_public_suggestion(crate_name) + }) + .is_some(); + let machine_applicable = + rustc_machine_applicable || manifest_machine_applicable; count_diagnostic(&msg.level, options); if msg .code @@ -2243,30 +2259,32 @@ fn on_stderr_line_inner( } MessageFormat::Json { ansi, .. } => { - #[derive(serde::Deserialize, serde::Serialize)] - struct CompilerMessage<'a> { - rendered: String, - #[serde(flatten, borrow)] - other: std::collections::BTreeMap, serde_json::Value>, - code: Option>, - } - - #[derive(serde::Deserialize, serde::Serialize)] - struct DiagnosticCode<'a> { - code: String, - #[serde(flatten, borrow)] - other: std::collections::BTreeMap, serde_json::Value>, - } - - if let Ok(mut error) = - serde_json::from_str::>(compiler_message.get()) + if let Ok(mut error) = serde_json::from_str::(compiler_message.get()) { - let modified_diag = if error - .code - .as_ref() - .is_some_and(|c| c.code == "exported_private_dependencies") - { - add_pub_in_priv_diagnostic(&mut error.rendered) + let is_pub_in_priv = error + .get("code") + .and_then(|code| code.get("code")) + .and_then(|code| code.as_str()) + == Some("exported_private_dependencies"); + let modified_diag = if is_pub_in_priv { + let rendered = error + .get("rendered") + .and_then(|rendered| rendered.as_str()) + .map(str::to_owned); + if let Some(mut rendered) = rendered { + let modified = add_pub_in_priv_diagnostic(&mut rendered); + if modified { + error["rendered"] = serde_json::Value::String(rendered); + } + modified + } else { + false + } + } else { + false + }; + let added_suggestion = if is_pub_in_priv { + add_pub_in_priv_suggestion(&mut error) } else { false }; @@ -2274,10 +2292,17 @@ fn on_stderr_line_inner( // Remove color information from the rendered string if color is not // enabled. Cargo always asks for ANSI colors from rustc. This allows // cached replay to enable/disable colors without re-invoking rustc. - if !ansi { - error.rendered = anstream::adapter::strip_str(&error.rendered).to_string(); + if !ansi + && let Some(rendered) = error + .get("rendered") + .and_then(|rendered| rendered.as_str()) + .map(str::to_owned) + { + error["rendered"] = serde_json::Value::String( + anstream::adapter::strip_str(&rendered).to_string(), + ); } - if !ansi || modified_diag { + if !ansi || modified_diag || added_suggestion { let new_line = serde_json::to_string(&error)?; compiler_message = serde_json::value::RawValue::from_string(new_line)?; } @@ -2495,6 +2520,62 @@ impl ManifestErrorContext { } None } + + fn find_crate_public_suggestion(&self, unrenamed: &str) -> Option { + let Some(ref spans) = self.spans else { + return None; + }; + let Some(ref contents) = self.contents else { + return None; + }; + + let orig_name = self.rename_table.get(unrenamed)?.as_str(); + + if let Some((_k, v)) = get_key_value(&spans, &["dependencies", orig_name]) { + return public_dependency_suggestion_from_value(&self.path, contents, v); + } + + // The dependency could also be in a target-specific table, like + // [target.x86_64-unknown-linux-gnu.dependencies] or + // [target.'cfg(something)'.dependencies]. We filter out target tables + // that don't match a requested target or a requested cfg. + if let Some(target) = spans + .as_ref() + .get_ref() + .get("target") + .and_then(|t| t.as_ref().as_table()) + { + for (platform, platform_table) in target.iter() { + match platform.as_ref().parse::() { + Ok(Platform::Name(name)) => { + if !self.requested_target_names().any(|n| n == name) { + continue; + } + } + Ok(Platform::Cfg(cfg_expr)) => { + if !self.cfgs.iter().any(|cfgs| cfg_expr.matches(cfgs)) { + continue; + } + } + Err(_) => continue, + } + + let Some(platform_table) = platform_table.as_ref().as_table() else { + continue; + }; + + if let Some(deps) = platform_table + .get("dependencies") + .and_then(|d| d.as_ref().as_table()) + { + if let Some((_k, v)) = deps.get_key_value(orig_name) { + return public_dependency_suggestion_from_value(&self.path, contents, v); + } + } + } + } + None + } } /// Creates a unit of work that replays the cached compiler message. diff --git a/src/diagnostics/mod.rs b/src/diagnostics/mod.rs index aed35eff8ba..8ff1bc8a445 100644 --- a/src/diagnostics/mod.rs +++ b/src/diagnostics/mod.rs @@ -63,12 +63,18 @@ use crate::workspace::Workspace; use crate::workspace::{Edition, Features, MaybePackage, Package}; mod lint; +mod pub_priv; mod report; pub mod passes; pub mod rules; pub use lint::{Lint, LintGroup, LintLevel, LintLevelProduct, LintLevelSource}; +pub(crate) use pub_priv::{ + PublicDependencyManifest, PublicDependencySuggestion, + add_public_dependency_suggestion_to_diagnostic, exported_private_dependency_name, + public_dependency_suggestion_from_value, push_public_dependency_suggestion_to_diagnostic, +}; pub use report::{AsIndex, cwd_rel_path, get_key_value, get_key_value_span, workspace_rel_path}; pub use rules::{LINT_GROUPS, LINTS}; diff --git a/src/diagnostics/pub_priv.rs b/src/diagnostics/pub_priv.rs new file mode 100644 index 00000000000..fd9ae9fa40a --- /dev/null +++ b/src/diagnostics/pub_priv.rs @@ -0,0 +1,330 @@ +use std::ops::Range; +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; + +use cargo_util::paths; +use regex::Regex; +use serde_json::json; + +use crate::util::errors::CargoResult; + +pub(crate) struct PublicDependencyManifest { + path: PathBuf, + contents: String, + document: toml::Spanned>, +} + +pub(crate) struct PublicDependencySuggestion { + file_name: String, + span: Range, + replacement: String, + line_start: usize, + column_start: usize, + column_end: usize, + line_text: String, +} + +impl PublicDependencyManifest { + pub(crate) fn load(path: PathBuf) -> CargoResult { + let contents = paths::read(&path)?; + let document = crate::workspace::parser::parse_document(&contents)?; + let document = crate::workspace::parser::make_document_owned(document); + Ok(Self { + path, + contents, + document, + }) + } + + pub(crate) fn find_public_suggestion( + &self, + unrenamed: &str, + ) -> Option { + let mut candidates = Vec::new(); + if let Some(deps) = self + .document + .get_ref() + .get("dependencies") + .and_then(|d| d.as_ref().as_table()) + { + self.find_in_dependency_table(deps, unrenamed, &mut candidates); + } + + if let Some(target) = self + .document + .get_ref() + .get("target") + .and_then(|t| t.as_ref().as_table()) + { + for (_, platform_table) in target.iter() { + let Some(platform_table) = platform_table.as_ref().as_table() else { + continue; + }; + let Some(deps) = platform_table + .get("dependencies") + .and_then(|d| d.as_ref().as_table()) + else { + continue; + }; + self.find_in_dependency_table(deps, unrenamed, &mut candidates); + } + } + + candidates.sort_by_key(|candidate| { + ( + candidate.file_name.clone(), + candidate.span.start, + candidate.span.end, + ) + }); + candidates.dedup_by(|a, b| a.file_name == b.file_name && a.span == b.span); + if candidates.len() == 1 { + candidates.pop() + } else { + None + } + } + + fn find_in_dependency_table( + &self, + deps: &toml::de::DeTable<'static>, + unrenamed: &str, + candidates: &mut Vec, + ) { + for (key, value) in deps.iter() { + if dependency_matches(key, value, unrenamed) { + if let Some(suggestion) = + public_dependency_suggestion_from_value(&self.path, &self.contents, value) + { + candidates.push(suggestion); + } + } + } + } +} + +impl PublicDependencySuggestion { + pub(crate) fn to_diagnostic_child(&self, crate_name: &str) -> serde_json::Value { + json!({ + "message": format!("mark dependency `{crate_name}` as public"), + "code": null, + "level": "help", + "spans": [{ + "file_name": self.file_name.clone(), + "byte_start": self.span.start, + "byte_end": self.span.end, + "line_start": self.line_start, + "line_end": self.line_start, + "column_start": self.column_start, + "column_end": self.column_end, + "is_primary": true, + "text": [{ + "text": self.line_text.clone(), + "highlight_start": self.column_start, + "highlight_end": self.column_end, + }], + "label": "mark as public", + "suggested_replacement": self.replacement.clone(), + "suggestion_applicability": "MachineApplicable", + "expansion": null, + }], + "children": [], + "rendered": null, + }) + } +} + +pub(crate) fn exported_private_dependency_name(message: &str) -> Option<&str> { + // rustc currently emits messages like: + // - type `FromPriv` from private dependency 'priv_dep' in public interface + // - struct `FromPriv` from private dependency 'priv_dep' is re-exported + static PRIV_DEP_REGEX: LazyLock = + LazyLock::new(|| Regex::new("from private dependency '([A-Za-z0-9-_]+)'").unwrap()); + PRIV_DEP_REGEX + .captures(message) + .and_then(|captures| captures.get(1)) + .map(|matched| matched.as_str()) +} + +pub(crate) fn add_public_dependency_suggestion_to_diagnostic( + diagnostic: &mut serde_json::Value, + manifest: &PublicDependencyManifest, +) -> bool { + if !is_exported_private_dependencies(diagnostic) { + return false; + } + + let Some(crate_name) = diagnostic + .get("message") + .and_then(|message| message.as_str()) + .and_then(exported_private_dependency_name) + .map(str::to_owned) + else { + return false; + }; + + let Some(suggestion) = manifest.find_public_suggestion(&crate_name) else { + return false; + }; + + push_public_dependency_suggestion_to_diagnostic(diagnostic, &crate_name, suggestion) +} + +pub(crate) fn push_public_dependency_suggestion_to_diagnostic( + diagnostic: &mut serde_json::Value, + crate_name: &str, + suggestion: PublicDependencySuggestion, +) -> bool { + if diagnostic.get("children").is_none() { + diagnostic["children"] = json!([]); + } + let Some(children) = diagnostic + .get_mut("children") + .and_then(|children| children.as_array_mut()) + else { + return false; + }; + children.push(suggestion.to_diagnostic_child(crate_name)); + true +} + +pub(crate) fn public_dependency_suggestion_from_value( + path: &Path, + contents: &str, + value: &toml::Spanned>, +) -> Option { + match value.get_ref() { + toml::de::DeValue::String(_) => { + let span = value.span(); + let version = contents.get(span.clone())?; + suggestion_from_replacement( + path, + contents, + span, + format!("{{ version = {version}, public = true }}"), + ) + } + _ => { + let table = value.get_ref().as_table()?; + if let Some(public) = table.get("public") { + return suggestion_from_replacement(path, contents, public.span(), "true".into()); + } + if table.get("workspace").is_some() { + return None; + } + + let span = value.span(); + let value_text = contents.get(span.clone())?; + if !value_text.trim_start().starts_with('{') || !value_text.trim_end().ends_with('}') { + return None; + } + + let brace_pos = value_text.rfind('}')?; + let insert_at = value_text[..brace_pos].trim_end().len(); + let before = &value_text[..insert_at]; + let after = &value_text[insert_at..]; + let separator = if before.trim_end().ends_with('{') { + " public = true" + } else { + ", public = true" + }; + suggestion_from_replacement(path, contents, span, format!("{before}{separator}{after}")) + } + } +} + +fn is_exported_private_dependencies(diagnostic: &serde_json::Value) -> bool { + diagnostic + .get("code") + .and_then(|code| code.get("code")) + .and_then(|code| code.as_str()) + == Some("exported_private_dependencies") +} + +fn dependency_matches( + key: &toml::Spanned>, + value: &toml::Spanned>, + unrenamed: &str, +) -> bool { + if key.as_ref() == unrenamed { + return true; + } + value + .get_ref() + .as_table() + .and_then(|table| table.get("package")) + .and_then(spanned_string) + == Some(unrenamed) +} + +fn spanned_string<'a>(value: &'a toml::Spanned>) -> Option<&'a str> { + match value.get_ref() { + toml::de::DeValue::String(value) => Some(value.as_ref()), + _ => None, + } +} + +fn suggestion_from_replacement( + path: &Path, + contents: &str, + span: Range, + replacement: String, +) -> Option { + let line = line_info(contents, span.clone())?; + Some(PublicDependencySuggestion { + file_name: path.display().to_string(), + span, + replacement, + line_start: line.line_start, + column_start: line.column_start, + column_end: line.column_end, + line_text: line.text, + }) +} + +struct LineInfo { + line_start: usize, + column_start: usize, + column_end: usize, + text: String, +} + +fn line_info(contents: &str, span: Range) -> Option { + if span.start > span.end + || span.end > contents.len() + || !contents.is_char_boundary(span.start) + || !contents.is_char_boundary(span.end) + { + return None; + } + + let line_start_byte = contents[..span.start] + .rfind('\n') + .map(|pos| pos + 1) + .unwrap_or(0); + let line_end_byte = contents[span.end..] + .find('\n') + .map(|pos| span.end + pos) + .unwrap_or(contents.len()); + if !contents.is_char_boundary(line_start_byte) || !contents.is_char_boundary(line_end_byte) { + return None; + } + + let line_start = contents[..line_start_byte] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1; + let column_start = contents[line_start_byte..span.start].chars().count() + 1; + let column_end = contents[line_start_byte..span.end].chars().count() + 1; + let text = contents[line_start_byte..line_end_byte] + .trim_end_matches('\r') + .to_string(); + + Some(LineInfo { + line_start, + column_start, + column_end, + text, + }) +} diff --git a/src/ops/cargo_fix/mod.rs b/src/ops/cargo_fix/mod.rs index 4df6c69502d..b7d381e51e7 100644 --- a/src/ops/cargo_fix/mod.rs +++ b/src/ops/cargo_fix/mod.rs @@ -54,6 +54,9 @@ use tracing::{debug, trace, warn}; pub use self::fix_edition::fix_edition; use crate::compiler::RustcTargetData; +use crate::diagnostics::{ + PublicDependencyManifest, add_public_dependency_suggestion_to_diagnostic, +}; use crate::ops::resolve::WorkspaceResolve; use crate::ops::{self, CompileOptions}; use crate::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor}; @@ -1015,13 +1018,22 @@ fn rustfix_and_fix( // Sift through the output of the compiler to look for JSON messages. // indicating fixes that we can apply. let stderr = str::from_utf8(&output.stderr).context("failed to parse rustc stderr as UTF-8")?; + let public_dependency_manifest = public_dependency_manifest_for_fix(args); let suggestions = stderr .lines() .filter(|x| !x.is_empty()) .inspect(|y| trace!("line: {}", y)) // Parse each line of stderr, ignoring errors, as they may not all be JSON. - .filter_map(|line| serde_json::from_str::(line).ok()) + .filter_map(|line| { + if let Some(manifest) = &public_dependency_manifest { + let mut diagnostic = serde_json::from_str::(line).ok()?; + add_public_dependency_suggestion_to_diagnostic(&mut diagnostic, manifest); + serde_json::from_value::(diagnostic).ok() + } else { + serde_json::from_str::(line).ok() + } + }) // From each diagnostic, try to extract suggestions from rustc. .filter_map(|diag| rustfix::collect_suggestions(&diag, &only, fix_mode)); @@ -1131,6 +1143,28 @@ fn rustfix_and_fix( Ok((output, made_changes)) } +fn public_dependency_manifest_for_fix(args: &FixArgs) -> Option { + let manifest_path = cargo_manifest_path_for_fix(args)?; + PublicDependencyManifest::load(manifest_path).ok() +} + +fn cargo_manifest_path_for_fix(args: &FixArgs) -> Option { + #[expect( + clippy::disallowed_methods, + reason = "rustc proxy receives the environment prepared for the compile" + )] + if let Some(manifest_dir) = env::var_os("CARGO_MANIFEST_DIR") { + return Some(PathBuf::from(manifest_dir).join("Cargo.toml")); + } + + args.file.parent().and_then(|parent| { + parent.ancestors().find_map(|ancestor| { + let manifest_path = ancestor.join("Cargo.toml"); + manifest_path.exists().then_some(manifest_path) + }) + }) +} + fn exit_with(status: ExitStatus) -> ! { #[cfg(unix)] { diff --git a/src/workspace/parser/mod.rs b/src/workspace/parser/mod.rs index e42d5274bd6..54b9f817704 100644 --- a/src/workspace/parser/mod.rs +++ b/src/workspace/parser/mod.rs @@ -157,7 +157,7 @@ pub fn read_manifest( /// Transform the parsed TOML document so that all its values are owned, so that it has a 'static /// lifetime, to make it easier to work with it. -fn make_document_owned( +pub(crate) fn make_document_owned( mut document: toml::Spanned>, ) -> toml::Spanned> { document.get_mut().make_owned(); @@ -185,7 +185,9 @@ fn read_toml_string(path: &Path, is_embedded: bool, gctx: &GlobalContext) -> Car } #[tracing::instrument(skip_all)] -fn parse_document(contents: &str) -> Result>, toml::de::Error> { +pub(crate) fn parse_document( + contents: &str, +) -> Result>, toml::de::Error> { toml::de::DeTable::parse(&contents) } diff --git a/tests/testsuite/pub_priv.rs b/tests/testsuite/pub_priv.rs index 8d7a1b658e0..6600a920100 100644 --- a/tests/testsuite/pub_priv.rs +++ b/tests/testsuite/pub_priv.rs @@ -45,6 +45,120 @@ src/lib.rs:3:13: [WARNING] type `FromPriv` from private dependency 'priv_dep' in .run(); } +#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")] +fn fix_exported_private_dependency() { + Package::new("priv_dep", "0.1.0") + .file("src/lib.rs", "pub struct FromPriv;") + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + priv_dep = "0.1.0" + "#, + ) + .file( + "src/lib.rs", + " + extern crate priv_dep; + pub fn use_priv(_: priv_dep::FromPriv) {} + ", + ) + .build(); + + p.cargo("fix --lib -Zpublic-dependency --allow-no-vcs") + .masquerade_as_nightly_cargo(&["public-dependency"]) + .with_stderr_contains("[FIXED] [..]Cargo.toml (1 fix)") + .run(); + + assert!( + p.read_file("Cargo.toml") + .contains(r#"priv_dep = { version = "0.1.0", public = true }"#) + ); +} + +#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")] +fn fix_exported_private_renamed_dependency() { + Package::new("priv_dep", "0.1.0") + .file("src/lib.rs", "pub struct FromPriv;") + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + renamed_dep = {version = "0.1.0", package = "priv_dep" } + "#, + ) + .file( + "src/lib.rs", + " + extern crate renamed_dep; + pub fn use_priv(_: renamed_dep::FromPriv) {} + ", + ) + .build(); + + p.cargo("fix --lib -Zpublic-dependency --allow-no-vcs") + .masquerade_as_nightly_cargo(&["public-dependency"]) + .with_stderr_contains("[FIXED] [..]Cargo.toml (1 fix)") + .run(); + + assert!( + p.read_file("Cargo.toml") + .contains(r#"renamed_dep = {version = "0.1.0", package = "priv_dep", public = true }"#) + ); +} + +#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")] +fn check_exported_private_dependency_mentions_cargo_fix() { + Package::new("priv_dep", "0.1.0") + .file("src/lib.rs", "pub struct FromPriv;") + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + priv_dep = "0.1.0" + "#, + ) + .file( + "src/lib.rs", + " + extern crate priv_dep; + pub fn use_priv(_: priv_dep::FromPriv) {} + ", + ) + .build(); + + p.cargo("check -Zpublic-dependency") + .masquerade_as_nightly_cargo(&["public-dependency"]) + .with_stderr_contains( + "[WARNING] `foo` (lib) generated 1 warning (run `cargo fix --lib -p foo` to apply 1 suggestion)", + ) + .run(); +} + #[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")] fn exported_pub_dep() { Package::new("pub_dep", "0.1.0") @@ -746,7 +860,7 @@ src/lib.rs:3:13: [WARNING] type `FromDep` from private dependency 'dep' in publi .with_stderr_data(str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) src/lib.rs:3:13: [WARNING] type `FromDep` from private dependency 'dep' in public interface -[WARNING] `foo` (lib) generated 1 warning +[WARNING] `foo` (lib) generated 1 warning (run `cargo fix --lib -p foo` to apply 1 suggestion) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]])