1use rustc_hash::FxHashSet;
2use std::fmt;
3
4use enum_iterator::Sequence;
5use enum_iterator::all;
6pub use ignore::Ignore;
7use ignore::find_ignores;
8use ignore::has_disable_assume_in_transaction;
9use ignore_index::IgnoreIndex;
10use rowan::TextRange;
11use rowan::TextSize;
12use serde::Deserialize;
13
14use squawk_syntax::SyntaxNode;
15use squawk_syntax::{Parse, SourceFile};
16
17pub use version::Version;
18
19pub mod analyze;
20pub mod ignore;
21mod ignore_index;
22mod version;
23mod visitors;
24
25mod rules;
26
27#[cfg(test)]
28mod test_utils;
29use rules::adding_field_with_default;
30use rules::adding_foreign_key_constraint;
31use rules::adding_not_null_field;
32use rules::adding_primary_key_constraint;
33use rules::adding_required_field;
34use rules::ban_alter_domain_with_add_constraint;
35use rules::ban_char_field;
36use rules::ban_concurrent_index_creation_in_transaction;
37use rules::ban_create_domain_with_constraint;
38use rules::ban_drop_column;
39use rules::ban_drop_database;
40use rules::ban_drop_not_null;
41use rules::ban_drop_table;
42use rules::ban_truncate_cascade;
43use rules::ban_uncommitted_transaction;
44use rules::changing_column_type;
45use rules::constraint_missing_not_valid;
46use rules::disallow_unique_constraint;
47use rules::identifier_too_long;
48use rules::prefer_bigint_over_int;
49use rules::prefer_bigint_over_smallint;
50use rules::prefer_identity;
51use rules::prefer_repack;
52use rules::prefer_robust_stmts;
53use rules::prefer_text_field;
54use rules::prefer_timestamptz;
55use rules::renaming_column;
56use rules::renaming_table;
57use rules::require_concurrent_index_creation;
58use rules::require_concurrent_index_deletion;
59use rules::require_concurrent_partition_detach;
60use rules::require_concurrent_reindex;
61use rules::require_enum_value_ordering;
62use rules::require_table_schema;
63use rules::require_timeout_settings;
64use rules::transaction_nesting;
65#[derive(Debug, PartialEq, Clone, Copy, Hash, Eq, Sequence)]
68pub enum Rule {
69 RequireConcurrentIndexCreation,
70 RequireConcurrentIndexDeletion,
71 ConstraintMissingNotValid,
72 AddingFieldWithDefault,
73 AddingForeignKeyConstraint,
74 ChangingColumnType,
75 AddingNotNullableField,
76 AddingSerialPrimaryKeyField,
77 RenamingColumn,
78 RenamingTable,
79 DisallowedUniqueConstraint,
80 BanDropDatabase,
81 PreferBigintOverInt,
82 PreferBigintOverSmallint,
83 PreferIdentity,
84 PreferRepack,
85 PreferRobustStmts,
86 PreferTextField,
87 PreferTimestampTz,
88 BanCharField,
89 BanDropColumn,
90 BanDropTable,
91 BanDropNotNull,
92 TransactionNesting,
93 AddingRequiredField,
94 BanConcurrentIndexCreationInTransaction,
95 UnusedIgnore,
96 BanCreateDomainWithConstraint,
97 BanAlterDomainWithAddConstraint,
98 BanTruncateCascade,
99 RequireTimeoutSettings,
100 BanUncommittedTransaction,
101 RequireEnumValueOrdering,
102 RequireTableSchema,
103 IdentifierTooLong,
104 RequireConcurrentPartitionDetach,
105 RequireConcurrentReindex,
106 RequireLockTimeout,
107 RequireStatementTimeout,
108 }
110
111impl Rule {
112 pub fn is_opt_in(&self) -> bool {
115 matches!(
117 self,
118 Rule::RequireTableSchema | Rule::RequireTimeoutSettings
119 )
120 }
121
122 pub fn expands_to(&self) -> &[Rule] {
124 match self {
125 Rule::RequireTimeoutSettings => {
126 &[Rule::RequireLockTimeout, Rule::RequireStatementTimeout]
127 }
128 _ => &[],
129 }
130 }
131}
132
133impl TryFrom<&str> for Rule {
134 type Error = String;
135
136 fn try_from(s: &str) -> Result<Self, Self::Error> {
137 match s {
138 "require-concurrent-index-creation" => Ok(Rule::RequireConcurrentIndexCreation),
139 "require-concurrent-index-deletion" => Ok(Rule::RequireConcurrentIndexDeletion),
140 "constraint-missing-not-valid" => Ok(Rule::ConstraintMissingNotValid),
141 "adding-field-with-default" => Ok(Rule::AddingFieldWithDefault),
142 "adding-foreign-key-constraint" => Ok(Rule::AddingForeignKeyConstraint),
143 "changing-column-type" => Ok(Rule::ChangingColumnType),
144 "adding-not-nullable-field" => Ok(Rule::AddingNotNullableField),
145 "adding-serial-primary-key-field" => Ok(Rule::AddingSerialPrimaryKeyField),
146 "renaming-column" => Ok(Rule::RenamingColumn),
147 "renaming-table" => Ok(Rule::RenamingTable),
148 "disallowed-unique-constraint" => Ok(Rule::DisallowedUniqueConstraint),
149 "ban-drop-database" => Ok(Rule::BanDropDatabase),
150 "prefer-bigint-over-int" => Ok(Rule::PreferBigintOverInt),
151 "prefer-bigint-over-smallint" => Ok(Rule::PreferBigintOverSmallint),
152 "prefer-identity" => Ok(Rule::PreferIdentity),
153 "prefer-repack" => Ok(Rule::PreferRepack),
154 "prefer-robust-stmts" => Ok(Rule::PreferRobustStmts),
155 "prefer-text-field" => Ok(Rule::PreferTextField),
156 "prefer-timestamptz" => Ok(Rule::PreferTimestampTz),
158 "prefer-timestamp-tz" => Ok(Rule::PreferTimestampTz),
159 "ban-char-field" => Ok(Rule::BanCharField),
160 "ban-drop-column" => Ok(Rule::BanDropColumn),
161 "ban-drop-table" => Ok(Rule::BanDropTable),
162 "ban-drop-not-null" => Ok(Rule::BanDropNotNull),
163 "transaction-nesting" => Ok(Rule::TransactionNesting),
164 "adding-required-field" => Ok(Rule::AddingRequiredField),
165 "ban-concurrent-index-creation-in-transaction" => {
166 Ok(Rule::BanConcurrentIndexCreationInTransaction)
167 }
168 "ban-create-domain-with-constraint" => Ok(Rule::BanCreateDomainWithConstraint),
169 "ban-alter-domain-with-add-constraint" => Ok(Rule::BanAlterDomainWithAddConstraint),
170 "ban-truncate-cascade" => Ok(Rule::BanTruncateCascade),
171 "require-timeout-settings" => Ok(Rule::RequireTimeoutSettings),
172 "ban-uncommitted-transaction" => Ok(Rule::BanUncommittedTransaction),
173 "require-enum-value-ordering" => Ok(Rule::RequireEnumValueOrdering),
174 "require-table-schema" => Ok(Rule::RequireTableSchema),
175 "identifier-too-long" => Ok(Rule::IdentifierTooLong),
176 "require-concurrent-partition-detach" => Ok(Rule::RequireConcurrentPartitionDetach),
177 "require-concurrent-reindex" => Ok(Rule::RequireConcurrentReindex),
178 "require-lock-timeout" => Ok(Rule::RequireLockTimeout),
179 "require-statement-timeout" => Ok(Rule::RequireStatementTimeout),
180 _ => Err(format!("Unknown violation name: {s}")),
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct UnknownRuleName {
188 val: String,
189}
190
191impl std::fmt::Display for UnknownRuleName {
192 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
193 write!(f, "invalid rule name {}", self.val)
194 }
195}
196
197impl std::error::Error for UnknownRuleName {}
198
199impl std::str::FromStr for Rule {
200 type Err = UnknownRuleName;
201 fn from_str(s: &str) -> Result<Self, Self::Err> {
202 Rule::try_from(s).map_err(|_| UnknownRuleName { val: s.to_string() })
203 }
204}
205
206impl fmt::Display for Rule {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 let val = match &self {
209 Rule::RequireConcurrentIndexCreation => "require-concurrent-index-creation",
210 Rule::RequireConcurrentIndexDeletion => "require-concurrent-index-deletion",
211 Rule::ConstraintMissingNotValid => "constraint-missing-not-valid",
212 Rule::AddingFieldWithDefault => "adding-field-with-default",
213 Rule::AddingForeignKeyConstraint => "adding-foreign-key-constraint",
214 Rule::ChangingColumnType => "changing-column-type",
215 Rule::AddingNotNullableField => "adding-not-nullable-field",
216 Rule::AddingSerialPrimaryKeyField => "adding-serial-primary-key-field",
217 Rule::RenamingColumn => "renaming-column",
218 Rule::RenamingTable => "renaming-table",
219 Rule::DisallowedUniqueConstraint => "disallowed-unique-constraint",
220 Rule::BanDropDatabase => "ban-drop-database",
221 Rule::PreferBigintOverInt => "prefer-bigint-over-int",
222 Rule::PreferBigintOverSmallint => "prefer-bigint-over-smallint",
223 Rule::PreferIdentity => "prefer-identity",
224 Rule::PreferRepack => "prefer-repack",
225 Rule::PreferRobustStmts => "prefer-robust-stmts",
226 Rule::PreferTextField => "prefer-text-field",
227 Rule::PreferTimestampTz => "prefer-timestamp-tz",
228 Rule::BanCharField => "ban-char-field",
229 Rule::BanDropColumn => "ban-drop-column",
230 Rule::BanDropTable => "ban-drop-table",
231 Rule::BanDropNotNull => "ban-drop-not-null",
232 Rule::TransactionNesting => "transaction-nesting",
233 Rule::AddingRequiredField => "adding-required-field",
234 Rule::BanConcurrentIndexCreationInTransaction => {
235 "ban-concurrent-index-creation-in-transaction"
236 }
237 Rule::BanCreateDomainWithConstraint => "ban-create-domain-with-constraint",
238 Rule::UnusedIgnore => "unused-ignore",
239 Rule::BanAlterDomainWithAddConstraint => "ban-alter-domain-with-add-constraint",
240 Rule::BanTruncateCascade => "ban-truncate-cascade",
241 Rule::RequireTimeoutSettings => "require-timeout-settings",
242 Rule::BanUncommittedTransaction => "ban-uncommitted-transaction",
243 Rule::RequireEnumValueOrdering => "require-enum-value-ordering",
244 Rule::RequireTableSchema => "require-table-schema",
245 Rule::IdentifierTooLong => "identifier-too-long",
246 Rule::RequireConcurrentPartitionDetach => "require-concurrent-partition-detach",
247 Rule::RequireConcurrentReindex => "require-concurrent-reindex",
248 Rule::RequireLockTimeout => "require-lock-timeout",
249 Rule::RequireStatementTimeout => "require-statement-timeout",
250 };
252 write!(f, "{val}")
253 }
254}
255
256impl<'de> Deserialize<'de> for Rule {
257 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
258 where
259 D: serde::Deserializer<'de>,
260 {
261 let s = String::deserialize(deserializer)?;
262 s.parse().map_err(serde::de::Error::custom)
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct Fix {
268 pub title: String,
269 pub edits: Vec<Edit>,
270}
271
272impl Fix {
273 fn new<T: Into<String>>(title: T, edits: Vec<Edit>) -> Fix {
274 Fix {
275 title: title.into(),
276 edits,
277 }
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct Edit {
283 pub text_range: TextRange,
284 pub text: Option<String>,
286}
287impl Edit {
288 pub fn insert<T: Into<String>>(text: T, at: TextSize) -> Self {
289 Self {
290 text_range: TextRange::new(at, at),
291 text: Some(text.into()),
292 }
293 }
294 pub fn replace<T: Into<String>>(text_range: TextRange, text: T) -> Self {
295 Self {
296 text_range,
297 text: Some(text.into()),
298 }
299 }
300 pub fn delete(text_range: TextRange) -> Self {
301 Self {
302 text_range,
303 text: None,
304 }
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct Violation {
310 pub code: Rule,
312 pub message: String,
313 pub text_range: TextRange,
314 pub help: Option<String>,
315 pub fix: Option<Fix>,
316}
317
318impl Violation {
319 #[must_use]
320 pub fn for_node(code: Rule, message: String, node: &SyntaxNode) -> Self {
321 let range = node.text_range();
322
323 let start = node
324 .children_with_tokens()
325 .find(|x| !x.kind().is_trivia())
326 .map(|x| x.text_range().start())
327 .unwrap_or_else(|| range.start());
329
330 Self {
331 code,
332 text_range: TextRange::new(start, range.end()),
333 message,
334 help: None,
335 fix: None,
336 }
337 }
338
339 #[must_use]
340 pub fn for_range(code: Rule, message: String, text_range: TextRange) -> Self {
341 Self {
342 code,
343 text_range,
344 message,
345 help: None,
346 fix: None,
347 }
348 }
349
350 fn fix<F: Into<Option<Fix>>>(mut self, fix: F) -> Violation {
351 self.fix = fix.into();
352 self
353 }
354 fn help(mut self, help: impl Into<String>) -> Violation {
355 self.help = Some(help.into());
356 self
357 }
358}
359
360#[derive(Clone, Default)]
361pub struct LinterSettings {
362 pub pg_version: Version,
363 pub assume_in_transaction: bool,
364}
365
366pub struct Linter {
367 errors: Vec<Violation>,
368 ignores: Vec<Ignore>,
369 pub rules: FxHashSet<Rule>,
370 pub settings: LinterSettings,
371}
372
373impl Linter {
374 fn report(&mut self, error: Violation) {
375 self.errors.push(error);
376 }
377
378 fn ignore(&mut self, ignore: Ignore) {
379 self.ignores.push(ignore);
380 }
381
382 #[must_use]
383 pub fn lint(&mut self, file: &Parse<SourceFile>, text: &str) -> Vec<Violation> {
384 if has_disable_assume_in_transaction(&file.syntax_node()) {
385 self.settings.assume_in_transaction = false;
386 }
387
388 if self.rules.contains(&Rule::AddingFieldWithDefault) {
389 adding_field_with_default(self, file);
390 }
391 if self.rules.contains(&Rule::AddingForeignKeyConstraint) {
392 adding_foreign_key_constraint(self, file);
393 }
394 if self.rules.contains(&Rule::AddingNotNullableField) {
395 adding_not_null_field(self, file);
396 }
397 if self.rules.contains(&Rule::AddingSerialPrimaryKeyField) {
398 adding_primary_key_constraint(self, file);
399 }
400 if self.rules.contains(&Rule::AddingRequiredField) {
401 adding_required_field(self, file);
402 }
403 if self.rules.contains(&Rule::BanDropDatabase) {
404 ban_drop_database(self, file);
405 }
406 if self.rules.contains(&Rule::BanCharField) {
407 ban_char_field(self, file);
408 }
409 if self
410 .rules
411 .contains(&Rule::BanConcurrentIndexCreationInTransaction)
412 {
413 ban_concurrent_index_creation_in_transaction(self, file);
414 }
415 if self.rules.contains(&Rule::BanDropColumn) {
416 ban_drop_column(self, file);
417 }
418 if self.rules.contains(&Rule::BanDropNotNull) {
419 ban_drop_not_null(self, file);
420 }
421 if self.rules.contains(&Rule::BanDropTable) {
422 ban_drop_table(self, file);
423 }
424 if self.rules.contains(&Rule::ChangingColumnType) {
425 changing_column_type(self, file);
426 }
427 if self.rules.contains(&Rule::ConstraintMissingNotValid) {
428 constraint_missing_not_valid(self, file);
429 }
430 if self.rules.contains(&Rule::DisallowedUniqueConstraint) {
431 disallow_unique_constraint(self, file);
432 }
433 if self.rules.contains(&Rule::PreferBigintOverInt) {
434 prefer_bigint_over_int(self, file);
435 }
436 if self.rules.contains(&Rule::PreferBigintOverSmallint) {
437 prefer_bigint_over_smallint(self, file);
438 }
439 if self.rules.contains(&Rule::PreferIdentity) {
440 prefer_identity(self, file);
441 }
442 if self.rules.contains(&Rule::PreferRepack) {
443 prefer_repack(self, file);
444 }
445 if self.rules.contains(&Rule::PreferRobustStmts) {
446 prefer_robust_stmts(self, file);
447 }
448 if self.rules.contains(&Rule::PreferTextField) {
449 prefer_text_field(self, file);
450 }
451 if self.rules.contains(&Rule::PreferTimestampTz) {
452 prefer_timestamptz(self, file);
453 }
454 if self.rules.contains(&Rule::RenamingColumn) {
455 renaming_column(self, file);
456 }
457 if self.rules.contains(&Rule::RenamingTable) {
458 renaming_table(self, file);
459 }
460 if self.rules.contains(&Rule::RequireConcurrentIndexCreation) {
461 require_concurrent_index_creation(self, file);
462 }
463 if self.rules.contains(&Rule::RequireConcurrentIndexDeletion) {
464 require_concurrent_index_deletion(self, file);
465 }
466 if self.rules.contains(&Rule::BanCreateDomainWithConstraint) {
467 ban_create_domain_with_constraint(self, file);
468 }
469 if self.rules.contains(&Rule::BanAlterDomainWithAddConstraint) {
470 ban_alter_domain_with_add_constraint(self, file);
471 }
472 if self.rules.contains(&Rule::TransactionNesting) {
473 transaction_nesting(self, file);
474 }
475 if self.rules.contains(&Rule::BanTruncateCascade) {
476 ban_truncate_cascade(self, file);
477 }
478 if self.rules.contains(&Rule::RequireLockTimeout)
479 || self.rules.contains(&Rule::RequireStatementTimeout)
480 {
481 require_timeout_settings(self, file);
482 }
483 if self.rules.contains(&Rule::BanUncommittedTransaction) {
484 ban_uncommitted_transaction(self, file);
485 }
486 if self.rules.contains(&Rule::RequireEnumValueOrdering) {
487 require_enum_value_ordering(self, file);
488 }
489 if self.rules.contains(&Rule::RequireTableSchema) {
490 require_table_schema(self, file);
491 }
492 if self.rules.contains(&Rule::IdentifierTooLong) {
493 identifier_too_long(self, file);
494 }
495 if self.rules.contains(&Rule::RequireConcurrentPartitionDetach) {
496 require_concurrent_partition_detach(self, file);
497 }
498 if self.rules.contains(&Rule::RequireConcurrentReindex) {
499 require_concurrent_reindex(self, file);
500 }
501 find_ignores(self, &file.syntax_node());
505
506 self.errors(text)
507 }
508
509 fn errors(&mut self, text: &str) -> Vec<Violation> {
510 let ignore_index = IgnoreIndex::new(text, &self.ignores);
511 let mut errors: Vec<Violation> = self
512 .errors
513 .iter()
514 .filter(|err| !ignore_index.contains(err.text_range, err.code))
517 .cloned()
518 .collect::<Vec<_>>();
519 errors.sort_by_key(|x| x.text_range.start());
521 errors
522 }
523
524 fn default_rules() -> FxHashSet<Rule> {
525 all::<Rule>()
526 .filter(|r| !r.is_opt_in())
527 .collect::<FxHashSet<_>>()
528 }
529
530 pub fn with_default_rules() -> Self {
531 let rules = Linter::default_rules();
532 Linter::from(rules)
533 }
534
535 pub fn with_rules(include: &[Rule], exclude: &[Rule]) -> Self {
536 let mut default_rules = Linter::default_rules();
537
538 for rule in include {
539 default_rules.insert(*rule);
540 default_rules.extend(rule.expands_to());
541 }
542
543 for rule in exclude {
544 default_rules.remove(rule);
545 for expanded in rule.expands_to() {
546 default_rules.remove(expanded);
547 }
548 }
549
550 default_rules.retain(|rule| rule.expands_to().is_empty());
553
554 Linter::from(default_rules)
555 }
556
557 pub fn from(rules: impl IntoIterator<Item = Rule>) -> Self {
558 let mut rules: FxHashSet<Rule> = rules.into_iter().collect();
559 for rule in rules.clone() {
560 rules.extend(rule.expands_to());
561 }
562 Self {
563 errors: vec![],
564 ignores: vec![],
565 rules,
566 settings: Default::default(),
567 }
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use insta::assert_debug_snapshot;
574
575 use super::*;
576
577 #[test]
578 fn prefer_timestamp_aliases() {
579 let rule1: Rule = "prefer-timestamp-tz".parse().unwrap();
580 let rule2: Rule = "prefer-timestamptz".parse().unwrap();
581 assert_eq!(rule1, rule2);
582 assert_debug_snapshot!(rule1, @"PreferTimestampTz");
583 }
584
585 #[test]
586 fn invalid_rule_name() {
587 let result: Result<Rule, _> = "invalid-rule-name".parse();
588 assert!(result.is_err());
589 }
590
591 #[test]
592 fn with_rules_opt_in_disabled_by_default() {
593 let linter = Linter::with_rules(&[], &[]);
594 assert!(!linter.rules.contains(&Rule::RequireTableSchema));
595 }
596
597 #[test]
598 fn with_rules_opt_in_enabled_via_include() {
599 let linter = Linter::with_rules(&[Rule::RequireTableSchema], &[]);
600 assert!(linter.rules.contains(&Rule::RequireTableSchema));
601 }
602
603 #[test]
604 fn with_rules_exclude_takes_precedence_over_include() {
605 let linter = Linter::with_rules(&[Rule::RequireTableSchema], &[Rule::RequireTableSchema]);
606 assert!(!linter.rules.contains(&Rule::RequireTableSchema));
607 }
608
609 #[test]
610 fn with_rules_exclude_removes_default_rule() {
611 let linter = Linter::with_rules(&[], &[Rule::BanDropTable]);
612 assert!(!linter.rules.contains(&Rule::BanDropTable));
613 }
614
615 #[test]
616 fn require_timeout_settings_expands_to_granular_rules() {
617 let linter = Linter::from([Rule::RequireTimeoutSettings]);
618 assert!(linter.rules.contains(&Rule::RequireLockTimeout));
619 assert!(linter.rules.contains(&Rule::RequireStatementTimeout));
620 }
621
622 #[test]
623 fn with_rules_exclude_timeout_settings_removes_granular_rules() {
624 let linter = Linter::with_rules(&[], &[Rule::RequireTimeoutSettings]);
625 assert!(!linter.rules.contains(&Rule::RequireLockTimeout));
626 assert!(!linter.rules.contains(&Rule::RequireStatementTimeout));
627 }
628
629 #[test]
630 fn with_rules_exclude_granular_timeout_rule_keeps_other() {
631 let linter = Linter::with_rules(&[], &[Rule::RequireStatementTimeout]);
632 assert!(linter.rules.contains(&Rule::RequireLockTimeout));
633 assert!(!linter.rules.contains(&Rule::RequireStatementTimeout));
634 }
635
636 #[test]
637 fn with_rules_exclude_granular_rule_wins_over_included_alias() {
638 let linter = Linter::with_rules(
639 &[Rule::RequireTimeoutSettings],
640 &[Rule::RequireStatementTimeout],
641 );
642 assert!(linter.rules.contains(&Rule::RequireLockTimeout));
643 assert!(!linter.rules.contains(&Rule::RequireStatementTimeout));
644 }
645}