diff --git a/PLAN.md b/PLAN.md index bafe35c9..ffebe192 100644 --- a/PLAN.md +++ b/PLAN.md @@ -51,6 +51,18 @@ parse and warn, helps with copy pasting examples support Trino, BigQuery, Aurora DSLQ, etc. +### Check `format()` calls + +https://www.postgresql.org/docs/18/functions-string.html#FUNCTIONS-STRING-FORMAT + +```sql +SELECT format('Hello %s', 'World'); +-- ok + +SELECT format('Hello %s %s', 'World'); +-- error ^^ +``` + ### Formatter ```shell @@ -387,6 +399,24 @@ select count(*) from t where t.name in ('1 month', '2 month') -- type checks! ``` +### Rule: no-unncesssary-parens + +```sql +select (((x * 2))); +-- becomes +select x * 2; +``` + +```sql +-- ok, indexes on expressions require extra parens +create index foo on t((1)); + +-- ok +select (x * 2) + 4; +``` + +related: https://eslint.style/rules/no-extra-parens + ### Rule: dialect: now() to dest should support various fixes so people can write in one dialect of SQL and have it easily convert to the other one diff --git a/crates/squawk/src/config.rs b/crates/squawk/src/config.rs index 56901cf7..26f56404 100644 --- a/crates/squawk/src/config.rs +++ b/crates/squawk/src/config.rs @@ -137,4 +137,14 @@ fail_on_violations = true fs::write(&squawk_toml, file).expect("Unable to write file"); assert_debug_snapshot!(Config::parse(Some(squawk_toml.path().to_path_buf()))); } + #[test] + fn load_excluded_rules_with_alias() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +excluded_rules = ["prefer-timestamp-tz", "prefer-timestamptz"] + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(Config::parse(Some(squawk_toml.path().to_path_buf()))); + } } diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules_with_alias.snap b/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules_with_alias.snap new file mode 100644 index 00000000..4b601469 --- /dev/null +++ b/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules_with_alias.snap @@ -0,0 +1,20 @@ +--- +source: crates/squawk/src/config.rs +expression: "Config::parse(Some(squawk_toml.path().to_path_buf()))" +--- +Ok( + Some( + Config { + excluded_paths: [], + excluded_rules: [ + PreferTimestampTz, + PreferTimestampTz, + ], + pg_version: None, + assume_in_transaction: None, + upload_to_github: UploadToGitHubConfig { + fail_on_violations: None, + }, + }, + ), +) diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index ea9c24c2..b17d57b9 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -54,7 +54,7 @@ use rules::require_concurrent_index_deletion; use rules::transaction_nesting; // xtask:new-rule:rule-import -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Hash, Eq, Deserialize, Sequence)] +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Hash, Eq, Sequence)] pub enum Rule { #[serde(rename = "require-concurrent-index-creation")] RequireConcurrentIndexCreation, @@ -176,7 +176,7 @@ impl std::error::Error for UnknownRuleName {} impl std::str::FromStr for Rule { type Err = UnknownRuleName; fn from_str(s: &str) -> Result { - serde_plain::from_str(s).map_err(|_| UnknownRuleName { val: s.to_string() }) + Rule::try_from(s).map_err(|_| UnknownRuleName { val: s.to_string() }) } } @@ -220,6 +220,16 @@ impl fmt::Display for Rule { } } +impl<'de> Deserialize<'de> for Rule { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Fix { pub title: String, @@ -470,3 +480,24 @@ impl Linter { } } } + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn prefer_timestamp_aliases() { + let rule1: Rule = "prefer-timestamp-tz".parse().unwrap(); + let rule2: Rule = "prefer-timestamptz".parse().unwrap(); + assert_eq!(rule1, rule2); + assert_debug_snapshot!(rule1, @"PreferTimestampTz"); + } + + #[test] + fn invalid_rule_name() { + let result: Result = "invalid-rule-name".parse(); + assert!(result.is_err()); + } +}