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