Skip to content
Merged
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
24 changes: 2 additions & 22 deletions config/src/explain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use std::fmt::Write as _;

use crate::registry::{PropId, Registry};
use crate::registry::PropId;
use crate::resolve::Resolved;
use crate::source::SourceKind;
use crate::value::{one_line, shown};
Expand Down Expand Up @@ -121,34 +121,14 @@ pub fn explain(resolved: &Resolved, key: &str) -> Option<String> {
// reading it off the setting that replaced it printed nothing at all for the one case where it
// matters — and following the renames from there, because a notice can sit anywhere along a
// chain: `a` renamed to `b`, and `b` the one carrying the notice that says to use `c`.
let deprecated = deprecation_along(registry, found.renamed_from.unwrap_or(meta.key));
let deprecated = registry.deprecation(found.renamed_from.unwrap_or(meta.key));
if let Some(why) = deprecated {
let _ = writeln!(out, "\n deprecated: {}", one_line(why));
}

Some(out)
}

/// The first deprecation notice along the rename chain that starts at `key`.
///
/// Bounded by the number of settings there are, so a registry whose renames form a cycle stops
/// rather than following them forever — the same guard [`Registry::lookup`] uses, and for the same
/// reason: this is an authoring mistake, and hanging is a worse way to report one than nothing at
/// all. `usage-config-build` refuses such a registry outright.
fn deprecation_along(registry: Registry, key: &str) -> Option<&'static str> {
let mut current = registry.lookup_exact(key)?;
for _ in 0..registry.props.len() {
let meta = registry.get(current);
if let Some(why) = meta.deprecated {
return Some(why);
}
current = meta
.renamed_to
.and_then(|next| registry.lookup_exact(next))?;
}
None
}

/// Every warning the resolution produced, as lines.
///
/// Separate from [`explain`] because they answer different questions and belong in different
Expand Down
22 changes: 22 additions & 0 deletions config/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,28 @@ impl Registry {
.map(|index| PropId(index as u16))
}

/// The first deprecation notice along the rename chain that starts at `key`.
///
/// The chain, not the declaration named: `a` renamed to `b`, and `b` the one carrying the notice
/// that says to use `c`. A user who wrote `a` is being told the same thing either way, and which
/// release the notice was attached in is not something they can see.
///
/// Bounded by the number of settings there are, so a registry whose renames form a cycle stops
/// rather than following them forever — the same guard [`Registry::lookup`] uses, and for the
/// same reason: this is an authoring mistake, and hanging is a worse way to report one than
/// nothing at all. `usage-config-build` refuses such a registry outright.
pub fn deprecation(&self, key: &str) -> Option<&'static str> {
let mut current = self.lookup_exact(key)?;
for _ in 0..self.props.len() {
let meta = self.get(current);
if let Some(why) = meta.deprecated {
return Some(why);
}
current = meta.renamed_to.and_then(|next| self.lookup_exact(next))?;
}
None
}

/// The settings an environment variable sets, and the variable that set them.
///
/// Several names per setting are aliases in descending precedence, which the env layer
Expand Down
61 changes: 56 additions & 5 deletions config/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,6 @@ pub fn resolve(registry: Registry, layers: Layers<'_>) -> Result<Resolved, Layer
// key up and `LayerCtx` folded it, or as a raw id this loop folded just now.
// Keyed on the fold alone, a file layer's deprecated key was folded in silence.
let written_key = entry.renamed_from.unwrap_or(written.key);
let as_written = registry
.lookup_exact(written_key)
.map(|id| registry.get(id))
.unwrap_or(written);
if let Some(refusal) = refuse(meta.scope, &entry.origin) {
// `written_key`, like the two warnings below it: after `LayerCtx` folds a
// rename, `written.key` is the *replacement's* name, so a refused value was
Expand All @@ -195,7 +191,11 @@ pub fn resolve(registry: Registry, layers: Layers<'_>) -> Result<Resolved, Layer
);
continue;
}
if let Some(why) = as_written.deprecated {
// Along the chain rather than off the declaration written, which is what `explain` has
// always done: a notice can sit on a name further along, and reading only the one the
// user wrote meant `config explain` told them to stop using a key that running the CLI
// said nothing about.
if let Some(why) = registry.deprecation(written_key) {
resolved.warnings.push(
Warning::at(
format!("{written_key} is deprecated: {why}"),
Expand Down Expand Up @@ -1057,6 +1057,57 @@ mod tests {
);
}

#[test]
fn a_notice_further_along_a_chain_of_renames_is_still_given() {
// Two releases of renaming: `threads` became `concurrency`, which became `jobs` and carries
// the notice. `explain` walked the chain for it and the merge read only the declaration
// written, so `config explain threads` told a user to stop using a key that running the CLI
// said nothing about — one rule, two implementations, and the quieter one was the one a CLI
// actually surfaces.
static PROPS: &[PropMeta] = &[
PropMeta {
default: Some(Const::Int(1)),
..PropMeta::new("jobs", Ty::Uint)
},
PropMeta {
renamed_to: Some("jobs"),
deprecated: Some("Use jobs instead."),
..PropMeta::new("concurrency", Ty::Uint)
},
PropMeta {
renamed_to: Some("concurrency"),
..PropMeta::new("threads", Ty::Uint)
},
];
const CHAINED: Registry = Registry::new(PROPS);

struct Wrote;
impl Layer for Wrote {
fn source(&self) -> SourceKind {
SourceKind::FILE
}
fn load(&self, ctx: &LayerCtx) -> Result<LayerOutput, LayerError> {
let mut out = LayerOutput::new();
let origin = Origin::file("hk.toml", FileScope::Project);
match ctx.entry_for_key("threads", "8", origin) {
Ok(entry) => out.push(entry),
Err(warning) => out.warn(warning),
}
Ok(out)
}
}
let resolved = resolve(CHAINED, Layers::new().then(&Wrote)).expect("should resolve");
assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
let kinds: Vec<_> = resolved.warnings.iter().map(|w| w.kind).collect();
assert_eq!(kinds, vec![WarningKind::Deprecated, WarningKind::Renamed]);
// Named by what the user wrote, since that is the line in the file they would go and edit.
assert!(
resolved.warnings[0].message == "threads is deprecated: Use jobs instead.",
"{:?}",
resolved.warnings[0].message
);
}

#[test]
fn an_unknown_key_is_a_warning_rather_than_a_failure() {
// Newer config read by an older binary: the key it does not know is reported and the
Expand Down
14 changes: 14 additions & 0 deletions corpus/config/04-renames.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,17 @@ vector "a-chain-of-renames-resolves-to-its-end" doc="Two releases of renaming le
warning "renamed"
}
}

vector "a-notice-anywhere-along-a-chain-is-reported" doc="A deprecation notice may sit on a name further along than the one that was written, and it is still what the user is told." {
setting "jobs" type="uint" default=1
setting "concurrency" type="uint" renamed-to="jobs" deprecated="Use jobs instead."
setting "threads" type="uint" renamed-to="concurrency"
layer "file" id="hk.toml" {
value "threads" "8"
}
expect {
value "jobs" 8
warning "deprecated"
warning "renamed"
}
}