From 60820482471615d9dfb6d2fe19b78c5db9c0ea3d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 6 Oct 2025 14:20:08 -0500 Subject: [PATCH] fix(config): Consistently error on unused key Fixes #1324 --- crates/typos-cli/src/bin/typos-cli/args.rs | 1 + crates/typos-cli/src/config.rs | 35 +++++++++++++++---- .../tests/cmd/unknown-default-field.toml | 5 ++- .../tests/cmd/unknown-type-field.toml | 5 ++- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/crates/typos-cli/src/bin/typos-cli/args.rs b/crates/typos-cli/src/bin/typos-cli/args.rs index debd10a2..0c33154e 100644 --- a/crates/typos-cli/src/bin/typos-cli/args.rs +++ b/crates/typos-cli/src/bin/typos-cli/args.rs @@ -169,6 +169,7 @@ impl FileArgs { ..Default::default() }, extend_ignore_re: Default::default(), + _unused: Default::default(), } } diff --git a/crates/typos-cli/src/config.rs b/crates/typos-cli/src/config.rs index 9f69ee01..c4732c7d 100644 --- a/crates/typos-cli/src/config.rs +++ b/crates/typos-cli/src/config.rs @@ -88,7 +88,7 @@ impl Config { ) })?; - if path.file_name().unwrap() == CARGO_TOML { + let config = if path.file_name().unwrap() == CARGO_TOML { let config = toml::from_str::(&s)?; let typos = config .workspace @@ -96,27 +96,32 @@ impl Config { .or(config.package.and_then(|p| p.metadata.typos)); if let Some(typos) = typos { - Ok(Some(typos)) + typos } else { log::debug!( "No `package.metadata.typos` section found in `{CARGO_TOML}`, skipping" ); - Ok(None) + return Ok(None); } } else if path.file_name().unwrap() == PYPROJECT_TOML { let config = toml::from_str::(&s)?; if let Some(typos) = config.tool.typos { - Ok(Some(typos)) + typos } else { log::debug!("No `tool.typos` section found in `{PYPROJECT_TOML}`, skipping"); - Ok(None) + return Ok(None); } } else { - Self::from_toml(&s).map(Some) + Self::from_toml(&s)? + }; + if let Some(key) = config.unused().next() { + anyhow::bail!("unknown key `{key}`"); } + + Ok(Some(config)) } pub fn from_toml(data: &str) -> Result { @@ -139,6 +144,19 @@ impl Config { self.type_.update(&source.type_); self.overrides.update(&source.overrides); } + + fn unused(&self) -> impl Iterator + '_ { + self.default + ._unused + .keys() + .map(|k| format!("default.{k}")) + .chain(self.type_.patterns.iter().flat_map(|(name, glob)| { + glob.engine._unused.keys().map(|k| { + let name = name.clone(); + format!("type.{name}.{k}") + }) + })) + } } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] @@ -344,6 +362,9 @@ pub struct EngineConfig { #[serde(with = "serde_regex")] #[cfg_attr(feature = "unstable-schema", schemars(schema_with = "vec_string"))] pub extend_ignore_re: Vec, + #[serde(flatten)] + #[cfg_attr(feature = "unstable-schema", schemars(skip))] + pub _unused: toml::Table, } impl EngineConfig { @@ -356,6 +377,7 @@ impl EngineConfig { tokenizer: TokenizerConfig::from_defaults(), dict: DictConfig::from_defaults(), extend_ignore_re: Default::default(), + _unused: Default::default(), } } @@ -373,6 +395,7 @@ impl EngineConfig { self.dict.update(&source.dict); self.extend_ignore_re .extend(source.extend_ignore_re.iter().cloned()); + self._unused.extend(source._unused.clone()); } pub fn binary(&self) -> bool { diff --git a/crates/typos-cli/tests/cmd/unknown-default-field.toml b/crates/typos-cli/tests/cmd/unknown-default-field.toml index 324f41d7..d3c8fbb9 100644 --- a/crates/typos-cli/tests/cmd/unknown-default-field.toml +++ b/crates/typos-cli/tests/cmd/unknown-default-field.toml @@ -1,4 +1,7 @@ bin.name = "typos" stdin = "" stdout = "" -stderr = "" +stderr = """ +unknown key `default.default-field` +""" +status.code = 78 diff --git a/crates/typos-cli/tests/cmd/unknown-type-field.toml b/crates/typos-cli/tests/cmd/unknown-type-field.toml index 324f41d7..3e2051d0 100644 --- a/crates/typos-cli/tests/cmd/unknown-type-field.toml +++ b/crates/typos-cli/tests/cmd/unknown-type-field.toml @@ -1,4 +1,7 @@ bin.name = "typos" stdin = "" stdout = "" -stderr = "" +stderr = """ +unknown key `type.rust.rust-field` +""" +status.code = 78