squawk_linter/
lib.rs

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// xtask:new-rule:rule-import
56
57#[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    // xtask:new-rule:error-name
118}
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            // this is typo'd so we just support both
143            "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            // xtask:new-rule:str-name
158            _ => 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            // xtask:new-rule:variant-to-name
218        };
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    // TODO: should this be String instead?
261    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            // Not sure we actually hit this, but just being safe
278            .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        // xtask:new-rule:rule-call
422
423        // locate any ignores in the file
424        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            // TODO: we should have errors for when there was an ignore but that
435            // ignore didn't actually ignore anything
436            .filter(|err| !ignore_index.contains(err.text_range, err.code))
437            .cloned()
438            .collect::<Vec<_>>();
439        // ensure we order them by where they appear in the file
440        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}