Skip to main content

squawk_linter/
lib.rs

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// xtask:new-rule:rule-import
66
67#[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    // xtask:new-rule:error-name
109}
110
111impl Rule {
112    /// Rules that are opt-in are not enabled by default.
113    /// They must be explicitly included via configuration.
114    pub fn is_opt_in(&self) -> bool {
115        // require-timeout-settings is an alias, see `Rule::expands_to`
116        matches!(
117            self,
118            Rule::RequireTableSchema | Rule::RequireTimeoutSettings
119        )
120    }
121
122    /// Rules that are deprecated aliases for other rules.
123    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            // this is typo'd so we just support both
157            "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            // xtask:new-rule:str-name
181            _ => 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            // xtask:new-rule:variant-to-name
251        };
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    // TODO: does this need to be an Option?
285    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    // TODO: should this be String instead?
311    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            // Not sure we actually hit this, but just being safe
328            .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        // xtask:new-rule:rule-call
502
503        // locate any ignores in the file
504        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            // TODO: we should have errors for when there was an ignore but that
515            // ignore didn't actually ignore anything
516            .filter(|err| !ignore_index.contains(err.text_range, err.code))
517            .cloned()
518            .collect::<Vec<_>>();
519        // ensure we order them by where they appear in the file
520        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        // drop aliases so `Linter::from` doesn't expand them again and re-add
551        // excluded rules
552        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}