From d302b622e736179a4e5d895f8aa66b311d4501a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Wed, 30 Sep 2026 15:11:02 -0300 Subject: [PATCH 1/2] fix(core): name the language in per-language config key errors Unknown or non-object per-language limit keys now report languages..limits instead of a bare limits path. --- crates/core/src/config.rs | 88 +++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index f817999..9bbdd7e 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -232,19 +232,30 @@ fn validate_keys(value: &Value, path: &Path) -> Result<()> { "", path, )?; - nested_keys(object, "limits", &Metric::ALL.map(Metric::name), path)?; - nested_keys(object, "tests", &["patterns", "exempt"], path)?; + nested_keys(object, "", "limits", &Metric::ALL.map(Metric::name), path)?; + nested_keys(object, "", "tests", &["patterns", "exempt"], path)?; validate_test_exempt(object, path)?; - nested_keys(object, "hook", &["max_blocks"], path)?; + nested_keys(object, "", "hook", &["max_blocks"], path)?; validate_language_keys(object, path) } -fn nested_keys(root: &Map, key: &str, keys: &[&str], path: &Path) -> Result<()> { +fn nested_keys( + root: &Map, + parent: &str, + key: &str, + keys: &[&str], + path: &Path, +) -> Result<()> { if let Some(value) = root.get(key) { + let full = if parent.is_empty() { + key.to_string() + } else { + format!("{parent}.{key}") + }; let object = value .as_object() - .ok_or_else(|| anyhow::anyhow!("{key} in {} must be an object", path.display()))?; - allowed(object, keys, key, path)?; + .ok_or_else(|| anyhow::anyhow!("{full} in {} must be an object", path.display()))?; + allowed(object, keys, &full, path)?; } Ok(()) } @@ -281,8 +292,15 @@ fn validate_language_keys(root: &Map, path: &Path) -> Result<()> let object = value .as_object() .ok_or_else(|| anyhow::anyhow!("languages.{name} must be an object"))?; - allowed(object, &["limits"], &format!("languages.{name}"), path)?; - nested_keys(object, "limits", &Metric::ALL.map(Metric::name), path)?; + let prefix = format!("languages.{name}"); + allowed(object, &["limits"], &prefix, path)?; + nested_keys( + object, + &prefix, + "limits", + &Metric::ALL.map(Metric::name), + path, + )?; } Ok(()) } @@ -445,4 +463,58 @@ mod tests { .to_string(); assert!(error.contains("hook.blocks")); } + + #[test] + fn unknown_language_limit_key_names_full_path() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.json"); + fs::write( + &path, + r#"{"languages":{"go":{"limits":{"widgetdepth":1}}}}"#, + ) + .unwrap(); + let error = load_config(dir.path(), Some(&path)) + .unwrap_err() + .to_string(); + assert!(error.contains("`languages.go.limits.widgetdepth`")); + } + + #[test] + fn unknown_language_limit_key_names_the_language_with_the_typo() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.json"); + fs::write( + &path, + r#"{"languages":{"go":{"limits":{"depth":3}},"rust":{"limits":{"widgetdepth":1}}}}"#, + ) + .unwrap(); + let error = load_config(dir.path(), Some(&path)) + .unwrap_err() + .to_string(); + assert!(error.contains("`languages.rust.limits.widgetdepth`")); + assert!(!error.contains("languages.go")); + } + + #[test] + fn non_object_language_limits_names_full_path() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.json"); + fs::write(&path, r#"{"languages":{"go":{"limits":1}}}"#).unwrap(); + let error = load_config(dir.path(), Some(&path)) + .unwrap_err() + .to_string(); + assert!(error.contains("languages.go.limits in ") && error.contains("must be an object")); + } + + #[test] + fn unknown_top_level_limit_key_has_no_language_path() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.json"); + fs::write(&path, r#"{"limits":{"widgetdepth":1}}"#).unwrap(); + let error = load_config(dir.path(), Some(&path)) + .unwrap_err() + .to_string(); + assert!(error.contains("`limits.widgetdepth`")); + assert!(!error.contains("languages")); + } } From ab4c20f283a3f1a33dd289893594111d008a6d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Wed, 30 Sep 2026 15:14:29 -0300 Subject: [PATCH 2/2] test(core): tighten top-level key assertion and note #41 in release draft --- crates/core/src/config.rs | 3 +-- docs/releases/UNRELEASED.md | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 9bbdd7e..448f8bc 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -514,7 +514,6 @@ mod tests { let error = load_config(dir.path(), Some(&path)) .unwrap_err() .to_string(); - assert!(error.contains("`limits.widgetdepth`")); - assert!(!error.contains("languages")); + assert!(error.contains("key `limits.widgetdepth`")); } } diff --git a/docs/releases/UNRELEASED.md b/docs/releases/UNRELEASED.md index 595e3a1..4ac267d 100644 --- a/docs/releases/UNRELEASED.md +++ b/docs/releases/UNRELEASED.md @@ -4,7 +4,10 @@ ## Changes -- +- An unknown or non-object per-language limit key in `.pickcheck.json` is now + reported with its `languages.` path, for example + `languages.go.limits.widgetdepth`, instead of a bare `limits.widgetdepth`. + (#41) ## Validation