1use std::collections::HashSet;
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, Serialize};
12
13use squawk_syntax::SyntaxNode;
14use squawk_syntax::{Parse, SourceFile};
15
16pub use version::Version;
17
18pub mod ignore;
19mod ignore_index;
20mod version;
21mod visitors;
22
23mod rules;
24
25#[cfg(test)]
26mod test_utils;
27use rules::adding_field_with_default;
28use rules::adding_foreign_key_constraint;
29use rules::adding_not_null_field;
30use rules::adding_primary_key_constraint;
31use rules::adding_required_field;
32use rules::ban_alter_domain_with_add_constraint;
33use rules::ban_char_field;
34use rules::ban_concurrent_index_creation_in_transaction;
35use rules::ban_create_domain_with_constraint;
36use rules::ban_drop_column;
37use rules::ban_drop_database;
38use rules::ban_drop_not_null;
39use rules::ban_drop_table;
40use rules::ban_truncate_cascade;
41use rules::changing_column_type;
42use rules::constraint_missing_not_valid;
43use rules::disallow_unique_constraint;
44use rules::prefer_bigint_over_int;
45use rules::prefer_bigint_over_smallint;
46use rules::prefer_identity;
47use rules::prefer_robust_stmts;
48use rules::prefer_text_field;
49use rules::prefer_timestamptz;
50use rules::renaming_column;
51use rules::renaming_table;
52use rules::require_concurrent_index_creation;
53use rules::require_concurrent_index_deletion;
54use rules::transaction_nesting;
55#[derive(Debug, PartialEq, Clone, Copy, Serialize, Hash, Eq, Deserialize, Sequence)]
58pub enum Rule {
59 #[serde(rename = "require-concurrent-index-creation")]
60 RequireConcurrentIndexCreation,
61 #[serde(rename = "require-concurrent-index-deletion")]
62 RequireConcurrentIndexDeletion,
63 #[serde(rename = "constraint-missing-not-valid")]
64 ConstraintMissingNotValid,
65 #[serde(rename = "adding-field-with-default")]
66 AddingFieldWithDefault,
67 #[serde(rename = "adding-foreign-key-constraint")]
68 AddingForeignKeyConstraint,
69 #[serde(rename = "changing-column-type")]
70 ChangingColumnType,
71 #[serde(rename = "adding-not-nullable-field")]
72 AddingNotNullableField,
73 #[serde(rename = "adding-serial-primary-key-field")]
74 AddingSerialPrimaryKeyField,
75 #[serde(rename = "renaming-column")]
76 RenamingColumn,
77 #[serde(rename = "renaming-table")]
78 RenamingTable,
79 #[serde(rename = "disallowed-unique-constraint")]
80 DisallowedUniqueConstraint,
81 #[serde(rename = "ban-drop-database")]
82 BanDropDatabase,
83 #[serde(rename = "prefer-bigint-over-int")]
84 PreferBigintOverInt,
85 #[serde(rename = "prefer-bigint-over-smallint")]
86 PreferBigintOverSmallint,
87 #[serde(rename = "prefer-identity")]
88 PreferIdentity,
89 #[serde(rename = "prefer-robust-stmts")]
90 PreferRobustStmts,
91 #[serde(rename = "prefer-text-field")]
92 PreferTextField,
93 #[serde(rename = "prefer-timestamptz")]
94 PreferTimestampTz,
95 #[serde(rename = "ban-char-field")]
96 BanCharField,
97 #[serde(rename = "ban-drop-column")]
98 BanDropColumn,
99 #[serde(rename = "ban-drop-table")]
100 BanDropTable,
101 #[serde(rename = "ban-drop-not-null")]
102 BanDropNotNull,
103 #[serde(rename = "transaction-nesting")]
104 TransactionNesting,
105 #[serde(rename = "adding-required-field")]
106 AddingRequiredField,
107 #[serde(rename = "ban-concurrent-index-creation-in-transaction")]
108 BanConcurrentIndexCreationInTransaction,
109 #[serde(rename = "unused-ignore")]
110 UnusedIgnore,
111 #[serde(rename = "ban-create-domain-with-constraint")]
112 BanCreateDomainWithConstraint,
113 #[serde(rename = "ban-alter-domain-with-add-constraint")]
114 BanAlterDomainWithAddConstraint,
115 #[serde(rename = "ban-truncate-cascade")]
116 BanTruncateCascade,
117 }
119
120impl TryFrom<&str> for Rule {
121 type Error = String;
122
123 fn try_from(s: &str) -> Result<Self, Self::Error> {
124 match s {
125 "require-concurrent-index-creation" => Ok(Rule::RequireConcurrentIndexCreation),
126 "require-concurrent-index-deletion" => Ok(Rule::RequireConcurrentIndexDeletion),
127 "constraint-missing-not-valid" => Ok(Rule::ConstraintMissingNotValid),
128 "adding-field-with-default" => Ok(Rule::AddingFieldWithDefault),
129 "adding-foreign-key-constraint" => Ok(Rule::AddingForeignKeyConstraint),
130 "changing-column-type" => Ok(Rule::ChangingColumnType),
131 "adding-not-nullable-field" => Ok(Rule::AddingNotNullableField),
132 "adding-serial-primary-key-field" => Ok(Rule::AddingSerialPrimaryKeyField),
133 "renaming-column" => Ok(Rule::RenamingColumn),
134 "renaming-table" => Ok(Rule::RenamingTable),
135 "disallowed-unique-constraint" => Ok(Rule::DisallowedUniqueConstraint),
136 "ban-drop-database" => Ok(Rule::BanDropDatabase),
137 "prefer-bigint-over-int" => Ok(Rule::PreferBigintOverInt),
138 "prefer-bigint-over-smallint" => Ok(Rule::PreferBigintOverSmallint),
139 "prefer-identity" => Ok(Rule::PreferIdentity),
140 "prefer-robust-stmts" => Ok(Rule::PreferRobustStmts),
141 "prefer-text-field" => Ok(Rule::PreferTextField),
142 "prefer-timestamptz" => Ok(Rule::PreferTimestampTz),
144 "prefer-timestamp-tz" => Ok(Rule::PreferTimestampTz),
145 "ban-char-field" => Ok(Rule::BanCharField),
146 "ban-drop-column" => Ok(Rule::BanDropColumn),
147 "ban-drop-table" => Ok(Rule::BanDropTable),
148 "ban-drop-not-null" => Ok(Rule::BanDropNotNull),
149 "transaction-nesting" => Ok(Rule::TransactionNesting),
150 "adding-required-field" => Ok(Rule::AddingRequiredField),
151 "ban-concurrent-index-creation-in-transaction" => {
152 Ok(Rule::BanConcurrentIndexCreationInTransaction)
153 }
154 "ban-create-domain-with-constraint" => Ok(Rule::BanCreateDomainWithConstraint),
155 "ban-alter-domain-with-add-constraint" => Ok(Rule::BanAlterDomainWithAddConstraint),
156 "ban-truncate-cascade" => Ok(Rule::BanTruncateCascade),
157 _ => Err(format!("Unknown violation name: {s}")),
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct UnknownRuleName {
165 val: String,
166}
167
168impl std::fmt::Display for UnknownRuleName {
169 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
170 write!(f, "invalid rule name {}", self.val)
171 }
172}
173
174impl std::error::Error for UnknownRuleName {}
175
176impl std::str::FromStr for Rule {
177 type Err = UnknownRuleName;
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 serde_plain::from_str(s).map_err(|_| UnknownRuleName { val: s.to_string() })
180 }
181}
182
183impl fmt::Display for Rule {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 let val = match &self {
186 Rule::RequireConcurrentIndexCreation => "require-concurrent-index-creation",
187 Rule::RequireConcurrentIndexDeletion => "require-concurrent-index-deletion",
188 Rule::ConstraintMissingNotValid => "constraint-missing-not-valid",
189 Rule::AddingFieldWithDefault => "adding-field-with-default",
190 Rule::AddingForeignKeyConstraint => "adding-foreign-key-constraint",
191 Rule::ChangingColumnType => "changing-column-type",
192 Rule::AddingNotNullableField => "adding-not-nullable-field",
193 Rule::AddingSerialPrimaryKeyField => "adding-serial-primary-key-field",
194 Rule::RenamingColumn => "renaming-column",
195 Rule::RenamingTable => "renaming-table",
196 Rule::DisallowedUniqueConstraint => "disallowed-unique-constraint",
197 Rule::BanDropDatabase => "ban-drop-database",
198 Rule::PreferBigintOverInt => "prefer-bigint-over-int",
199 Rule::PreferBigintOverSmallint => "prefer-bigint-over-smallint",
200 Rule::PreferIdentity => "prefer-identity",
201 Rule::PreferRobustStmts => "prefer-robust-stmts",
202 Rule::PreferTextField => "prefer-text-field",
203 Rule::PreferTimestampTz => "prefer-timestamp-tz",
204 Rule::BanCharField => "ban-char-field",
205 Rule::BanDropColumn => "ban-drop-column",
206 Rule::BanDropTable => "ban-drop-table",
207 Rule::BanDropNotNull => "ban-drop-not-null",
208 Rule::TransactionNesting => "transaction-nesting",
209 Rule::AddingRequiredField => "adding-required-field",
210 Rule::BanConcurrentIndexCreationInTransaction => {
211 "ban-concurrent-index-creation-in-transaction"
212 }
213 Rule::BanCreateDomainWithConstraint => "ban-create-domain-with-constraint",
214 Rule::UnusedIgnore => "unused-ignore",
215 Rule::BanAlterDomainWithAddConstraint => "ban-alter-domain-with-add-constraint",
216 Rule::BanTruncateCascade => "ban-truncate-cascade",
217 };
219 write!(f, "{val}")
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct Fix {
225 pub title: String,
226 pub edits: Vec<Edit>,
227}
228
229impl Fix {
230 fn new<T: Into<String>>(title: T, edits: Vec<Edit>) -> Fix {
231 Fix {
232 title: title.into(),
233 edits,
234 }
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct Edit {
240 pub text_range: TextRange,
241 pub text: Option<String>,
242}
243impl Edit {
244 pub fn insert<T: Into<String>>(text: T, at: TextSize) -> Self {
245 Self {
246 text_range: TextRange::new(at, at),
247 text: Some(text.into()),
248 }
249 }
250 pub fn replace<T: Into<String>>(text_range: TextRange, text: T) -> Self {
251 Self {
252 text_range,
253 text: Some(text.into()),
254 }
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct Violation {
260 pub code: Rule,
262 pub message: String,
263 pub text_range: TextRange,
264 pub help: Option<String>,
265 pub fix: Option<Fix>,
266}
267
268impl Violation {
269 #[must_use]
270 pub fn for_node(code: Rule, message: String, node: &SyntaxNode) -> Self {
271 let range = node.text_range();
272
273 let start = node
274 .children_with_tokens()
275 .find(|x| !x.kind().is_trivia())
276 .map(|x| x.text_range().start())
277 .unwrap_or_else(|| range.start());
279
280 Self {
281 code,
282 text_range: TextRange::new(start, range.end()),
283 message,
284 help: None,
285 fix: None,
286 }
287 }
288
289 #[must_use]
290 pub fn for_range(code: Rule, message: String, text_range: TextRange) -> Self {
291 Self {
292 code,
293 text_range,
294 message,
295 help: None,
296 fix: None,
297 }
298 }
299
300 fn fix(mut self, fix: Option<Fix>) -> Violation {
301 self.fix = fix;
302 self
303 }
304 fn help(mut self, help: impl Into<String>) -> Violation {
305 self.help = Some(help.into());
306 self
307 }
308}
309
310#[derive(Default)]
311pub struct LinterSettings {
312 pub pg_version: Version,
313 pub assume_in_transaction: bool,
314}
315
316pub struct Linter {
317 errors: Vec<Violation>,
318 ignores: Vec<Ignore>,
319 pub rules: HashSet<Rule>,
320 pub settings: LinterSettings,
321}
322
323impl Linter {
324 fn report(&mut self, error: Violation) {
325 self.errors.push(error);
326 }
327
328 fn ignore(&mut self, ignore: Ignore) {
329 self.ignores.push(ignore);
330 }
331
332 #[must_use]
333 pub fn lint(&mut self, file: &Parse<SourceFile>, text: &str) -> Vec<Violation> {
334 if self.rules.contains(&Rule::AddingFieldWithDefault) {
335 adding_field_with_default(self, file);
336 }
337 if self.rules.contains(&Rule::AddingForeignKeyConstraint) {
338 adding_foreign_key_constraint(self, file);
339 }
340 if self.rules.contains(&Rule::AddingNotNullableField) {
341 adding_not_null_field(self, file);
342 }
343 if self.rules.contains(&Rule::AddingSerialPrimaryKeyField) {
344 adding_primary_key_constraint(self, file);
345 }
346 if self.rules.contains(&Rule::AddingRequiredField) {
347 adding_required_field(self, file);
348 }
349 if self.rules.contains(&Rule::BanDropDatabase) {
350 ban_drop_database(self, file);
351 }
352 if self.rules.contains(&Rule::BanCharField) {
353 ban_char_field(self, file);
354 }
355 if self
356 .rules
357 .contains(&Rule::BanConcurrentIndexCreationInTransaction)
358 {
359 ban_concurrent_index_creation_in_transaction(self, file);
360 }
361 if self.rules.contains(&Rule::BanDropColumn) {
362 ban_drop_column(self, file);
363 }
364 if self.rules.contains(&Rule::BanDropNotNull) {
365 ban_drop_not_null(self, file);
366 }
367 if self.rules.contains(&Rule::BanDropTable) {
368 ban_drop_table(self, file);
369 }
370 if self.rules.contains(&Rule::ChangingColumnType) {
371 changing_column_type(self, file);
372 }
373 if self.rules.contains(&Rule::ConstraintMissingNotValid) {
374 constraint_missing_not_valid(self, file);
375 }
376 if self.rules.contains(&Rule::DisallowedUniqueConstraint) {
377 disallow_unique_constraint(self, file);
378 }
379 if self.rules.contains(&Rule::PreferBigintOverInt) {
380 prefer_bigint_over_int(self, file);
381 }
382 if self.rules.contains(&Rule::PreferBigintOverSmallint) {
383 prefer_bigint_over_smallint(self, file);
384 }
385 if self.rules.contains(&Rule::PreferIdentity) {
386 prefer_identity(self, file);
387 }
388 if self.rules.contains(&Rule::PreferRobustStmts) {
389 prefer_robust_stmts(self, file);
390 }
391 if self.rules.contains(&Rule::PreferTextField) {
392 prefer_text_field(self, file);
393 }
394 if self.rules.contains(&Rule::PreferTimestampTz) {
395 prefer_timestamptz(self, file);
396 }
397 if self.rules.contains(&Rule::RenamingColumn) {
398 renaming_column(self, file);
399 }
400 if self.rules.contains(&Rule::RenamingTable) {
401 renaming_table(self, file);
402 }
403 if self.rules.contains(&Rule::RequireConcurrentIndexCreation) {
404 require_concurrent_index_creation(self, file);
405 }
406 if self.rules.contains(&Rule::RequireConcurrentIndexDeletion) {
407 require_concurrent_index_deletion(self, file);
408 }
409 if self.rules.contains(&Rule::BanCreateDomainWithConstraint) {
410 ban_create_domain_with_constraint(self, file);
411 }
412 if self.rules.contains(&Rule::BanAlterDomainWithAddConstraint) {
413 ban_alter_domain_with_add_constraint(self, file);
414 }
415 if self.rules.contains(&Rule::TransactionNesting) {
416 transaction_nesting(self, file);
417 }
418 if self.rules.contains(&Rule::BanTruncateCascade) {
419 ban_truncate_cascade(self, file);
420 }
421 find_ignores(self, &file.syntax_node());
425
426 self.errors(text)
427 }
428
429 fn errors(&mut self, text: &str) -> Vec<Violation> {
430 let ignore_index = IgnoreIndex::new(text, &self.ignores);
431 let mut errors: Vec<Violation> = self
432 .errors
433 .iter()
434 .filter(|err| !ignore_index.contains(err.text_range, err.code))
437 .cloned()
438 .collect::<Vec<_>>();
439 errors.sort_by_key(|x| x.text_range.start());
441 errors
442 }
443
444 pub fn with_all_rules() -> Self {
445 let rules = all::<Rule>().collect::<HashSet<_>>();
446 Linter::from(rules)
447 }
448
449 pub fn without_rules(exclude: &[Rule]) -> Self {
450 let all_rules = all::<Rule>().collect::<HashSet<_>>();
451 let mut exclude_set = HashSet::with_capacity(exclude.len());
452 for e in exclude {
453 exclude_set.insert(e);
454 }
455
456 let rules = all_rules
457 .into_iter()
458 .filter(|x| !exclude_set.contains(x))
459 .collect::<HashSet<_>>();
460
461 Linter::from(rules)
462 }
463
464 pub fn from(rules: impl Into<HashSet<Rule>>) -> Self {
465 Self {
466 errors: vec![],
467 ignores: vec![],
468 rules: rules.into(),
469 settings: LinterSettings::default(),
470 }
471 }
472}