From 700973c1f4600593558e1c8796fc5a51284e7d95 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 6 Oct 2025 14:07:24 -0500 Subject: [PATCH 1/4] test(config): Check unknown field behavior --- .../tests/cmd/unknown-default-field.in/_typos.toml | 2 ++ .../typos-cli/tests/cmd/unknown-default-field.toml | 4 ++++ .../tests/cmd/unknown-files-field.in/_typos.toml | 2 ++ crates/typos-cli/tests/cmd/unknown-files-field.toml | 12 ++++++++++++ .../tests/cmd/unknown-top-field.in/_typos.toml | 1 + crates/typos-cli/tests/cmd/unknown-top-field.toml | 12 ++++++++++++ .../tests/cmd/unknown-type-field.in/_typos.toml | 2 ++ crates/typos-cli/tests/cmd/unknown-type-field.toml | 4 ++++ 8 files changed, 39 insertions(+) create mode 100644 crates/typos-cli/tests/cmd/unknown-default-field.in/_typos.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-default-field.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-files-field.in/_typos.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-files-field.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-top-field.in/_typos.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-top-field.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-type-field.in/_typos.toml create mode 100644 crates/typos-cli/tests/cmd/unknown-type-field.toml diff --git a/crates/typos-cli/tests/cmd/unknown-default-field.in/_typos.toml b/crates/typos-cli/tests/cmd/unknown-default-field.in/_typos.toml new file mode 100644 index 00000000..fcbaf3ff --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-default-field.in/_typos.toml @@ -0,0 +1,2 @@ +[default] +default-field = "foo" diff --git a/crates/typos-cli/tests/cmd/unknown-default-field.toml b/crates/typos-cli/tests/cmd/unknown-default-field.toml new file mode 100644 index 00000000..324f41d7 --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-default-field.toml @@ -0,0 +1,4 @@ +bin.name = "typos" +stdin = "" +stdout = "" +stderr = "" diff --git a/crates/typos-cli/tests/cmd/unknown-files-field.in/_typos.toml b/crates/typos-cli/tests/cmd/unknown-files-field.in/_typos.toml new file mode 100644 index 00000000..4249a99c --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-files-field.in/_typos.toml @@ -0,0 +1,2 @@ +[files] +files-field = "foo" diff --git a/crates/typos-cli/tests/cmd/unknown-files-field.toml b/crates/typos-cli/tests/cmd/unknown-files-field.toml new file mode 100644 index 00000000..190f52bb --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-files-field.toml @@ -0,0 +1,12 @@ +bin.name = "typos" +stdin = "" +stdout = "" +stderr = """ +TOML parse error at line 2, column 1 + | +2 | files-field = "foo" + | ^^^^^^^^^^^ +unknown field `files-field`, expected one of `extend-exclude`, `ignore-hidden`, `ignore-files`, `ignore-dot`, `ignore-vcs`, `ignore-global`, `ignore-parent` + +""" +status.code = 78 diff --git a/crates/typos-cli/tests/cmd/unknown-top-field.in/_typos.toml b/crates/typos-cli/tests/cmd/unknown-top-field.in/_typos.toml new file mode 100644 index 00000000..6505bc8e --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-top-field.in/_typos.toml @@ -0,0 +1 @@ +top-field = "foo" diff --git a/crates/typos-cli/tests/cmd/unknown-top-field.toml b/crates/typos-cli/tests/cmd/unknown-top-field.toml new file mode 100644 index 00000000..50a05676 --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-top-field.toml @@ -0,0 +1,12 @@ +bin.name = "typos" +stdin = "" +stdout = "" +stderr = """ +TOML parse error at line 1, column 1 + | +1 | top-field = "foo" + | ^^^^^^^^^ +unknown field `top-field`, expected one of `files`, `default`, `type` + +""" +status.code = 78 diff --git a/crates/typos-cli/tests/cmd/unknown-type-field.in/_typos.toml b/crates/typos-cli/tests/cmd/unknown-type-field.in/_typos.toml new file mode 100644 index 00000000..82e65e65 --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-type-field.in/_typos.toml @@ -0,0 +1,2 @@ +[type.rust] +rust-field = "foo" diff --git a/crates/typos-cli/tests/cmd/unknown-type-field.toml b/crates/typos-cli/tests/cmd/unknown-type-field.toml new file mode 100644 index 00000000..324f41d7 --- /dev/null +++ b/crates/typos-cli/tests/cmd/unknown-type-field.toml @@ -0,0 +1,4 @@ +bin.name = "typos" +stdin = "" +stdout = "" +stderr = "" From 931fd92e9feeaafc67c9d1ddbf52a12044b0dc4b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 6 Oct 2025 14:14:08 -0500 Subject: [PATCH 2/4] test(config): Make asserts more informative --- Cargo.lock | 2 ++ crates/typos-cli/Cargo.toml | 2 +- crates/typos-cli/src/config.rs | 36 +++++++++++++++++++++++----------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c55293d0..2857f160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,6 +1314,8 @@ dependencies = [ "libc", "normalize-line-endings", "os_pipe", + "serde", + "serde_json", "similar", "snapbox-macros", "tempfile", diff --git a/crates/typos-cli/Cargo.toml b/crates/typos-cli/Cargo.toml index 7dca7896..7fc19805 100644 --- a/crates/typos-cli/Cargo.toml +++ b/crates/typos-cli/Cargo.toml @@ -84,7 +84,7 @@ supports-unicode = "3.0.0" assert_fs = "1.1" divan = "0.1.21" maplit = "1.0" -snapbox = "0.6.21" +snapbox = { version = "0.6.21", features = ["json"] } trycmd = "0.15.10" [[bench]] diff --git a/crates/typos-cli/src/config.rs b/crates/typos-cli/src/config.rs index 1be1ade5..4d3e8bfc 100644 --- a/crates/typos-cli/src/config.rs +++ b/crates/typos-cli/src/config.rs @@ -655,6 +655,8 @@ fn hashmap_string_t( #[cfg(test)] mod test { use super::*; + use snapbox::assert_data_eq; + use snapbox::prelude::*; #[cfg(feature = "unstable-schema")] #[test] @@ -668,11 +670,23 @@ mod test { fn test_from_defaults() { let null = Config::default(); let defaulted = Config::from_defaults(); - assert_ne!(defaulted, null); - assert_ne!(defaulted.files, null.files); - assert_ne!(defaulted.default, null.default); - assert_ne!(defaulted.default.tokenizer, null.default.tokenizer); - assert_ne!(defaulted.default.dict, null.default.dict); + assert_ne!(defaulted.clone().into_json(), null.clone().into_json()); + assert_ne!( + defaulted.files.clone().into_json(), + null.files.clone().into_json() + ); + assert_ne!( + defaulted.default.clone().into_json(), + null.default.clone().into_json() + ); + assert_ne!( + defaulted.default.tokenizer.clone().into_json(), + null.default.tokenizer.clone().into_json() + ); + assert_ne!( + defaulted.default.dict.clone().into_json(), + null.default.dict.clone().into_json() + ); } #[test] @@ -683,7 +697,7 @@ mod test { let mut actual = defaulted.clone(); actual.update(&null); - assert_eq!(actual, defaulted); + assert_data_eq!(actual.into_json(), defaulted.into_json()); } #[test] @@ -694,7 +708,7 @@ mod test { let mut actual = null; actual.update(&defaulted); - assert_eq!(actual, defaulted); + assert_data_eq!(actual.into_json(), defaulted.into_json()); } #[test] @@ -708,7 +722,7 @@ mod test { let mut actual = null; actual.update(&extended); - assert_eq!(actual, extended); + assert_data_eq!(actual.into_json(), extended.into_json()); } #[test] @@ -726,7 +740,7 @@ mod test { actual.update(&extended); let expected: Vec = vec!["*.foo".into(), "*.bar".into()]; - assert_eq!(actual.extend_glob, expected); + assert_data_eq!(actual.extend_glob.into_json(), expected.into_json()); } #[test] @@ -749,7 +763,7 @@ check-file = true }, ); let actual = Config::from_toml(input).unwrap(); - assert_eq!(actual, expected); + assert_data_eq!(actual.into_json(), expected.into_json()); } #[test] @@ -781,6 +795,6 @@ inout = "inout" }, ); let actual = Config::from_toml(input).unwrap(); - assert_eq!(actual, expected); + assert_data_eq!(actual.into_json(), expected.into_json()); } } From d8080f623f349d03ea1e887a1d0f25cd33fed755 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 6 Oct 2025 14:31:39 -0500 Subject: [PATCH 3/4] refactor(config): Remove unused PartialEq/Eq --- crates/typos-cli/src/config.rs | 59 +++++++--------------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/crates/typos-cli/src/config.rs b/crates/typos-cli/src/config.rs index 4d3e8bfc..9f69ee01 100644 --- a/crates/typos-cli/src/config.rs +++ b/crates/typos-cli/src/config.rs @@ -17,7 +17,7 @@ pub const SUPPORTED_FILE_NAMES: &[&str] = &[ const CARGO_TOML: &str = "Cargo.toml"; const PYPROJECT_TOML: &str = "pyproject.toml"; -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] #[serde(default)] #[serde(rename_all = "kebab-case")] @@ -31,7 +31,7 @@ pub struct Config { pub overrides: EngineConfig, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(default)] #[serde(rename_all = "kebab-case")] pub struct CargoTomlConfig { @@ -39,28 +39,28 @@ pub struct CargoTomlConfig { pub package: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(default)] #[serde(rename_all = "kebab-case")] pub struct CargoTomlPackage { pub metadata: CargoTomlMetadata, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(default)] #[serde(rename_all = "kebab-case")] pub struct CargoTomlMetadata { pub typos: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(default)] #[serde(rename_all = "kebab-case")] pub struct PyprojectTomlConfig { pub tool: PyprojectTomlTool, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(default)] #[serde(rename_all = "kebab-case")] pub struct PyprojectTomlTool { @@ -141,7 +141,7 @@ impl Config { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] #[serde(default)] #[serde(rename_all = "kebab-case")] @@ -232,7 +232,7 @@ impl Walk { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] #[serde(default)] #[serde(transparent)] @@ -306,7 +306,7 @@ impl TypeEngineConfig { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] //#[serde(deny_unknown_fields)] // Doesn't work with `flatten` #[serde(default)] #[serde(rename_all = "kebab-case")] @@ -392,24 +392,7 @@ impl EngineConfig { } } -impl PartialEq for EngineConfig { - fn eq(&self, rhs: &Self) -> bool { - self.binary == rhs.binary - && self.check_filename == rhs.check_filename - && self.check_file == rhs.check_file - && self.tokenizer == rhs.tokenizer - && self.dict == rhs.dict - && self - .extend_ignore_re - .iter() - .map(|r| r.as_str()) - .eq(rhs.extend_ignore_re.iter().map(|r| r.as_str())) - } -} - -impl Eq for EngineConfig {} - -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] #[serde(default)] #[serde(rename_all = "kebab-case")] @@ -556,27 +539,7 @@ fn find_project_files<'a>( .filter(|path| path.exists()) } -impl PartialEq for DictConfig { - fn eq(&self, rhs: &Self) -> bool { - self.locale == rhs.locale - && self - .extend_ignore_identifiers_re - .iter() - .map(|r| r.as_str()) - .eq(rhs.extend_ignore_identifiers_re.iter().map(|r| r.as_str())) - && self.extend_identifiers == rhs.extend_identifiers - && self - .extend_ignore_words_re - .iter() - .map(|r| r.as_str()) - .eq(rhs.extend_ignore_words_re.iter().map(|r| r.as_str())) - && self.extend_words == rhs.extend_words - } -} - -impl Eq for DictConfig {} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] #[derive(Default)] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] From 60820482471615d9dfb6d2fe19b78c5db9c0ea3d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 6 Oct 2025 14:20:08 -0500 Subject: [PATCH 4/4] 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