Skip to main content

alpm_lint/lint_rules/
store.rs

1//! Access and filtering to all registered lints.
2//!
3//! # Note
4//!
5//! All lints need to be registered in the private `LintStore::register` function when adding a new
6//! lint rule!
7
8use std::{
9    collections::{BTreeMap, btree_map},
10    fmt,
11};
12
13#[cfg(feature = "serde")]
14use alpm_lint_config::LintRuleConfigurationOptionName;
15use alpm_lint_config::{LintConfiguration, LintRuleConfiguration};
16#[cfg(feature = "serde")]
17use serde::Serialize;
18
19#[cfg(feature = "serde")]
20use crate::internal_prelude::LintGroup;
21use crate::{
22    ScopedName,
23    internal_prelude::{Level, LintRule, LintScope},
24    lint_rules::source_info::{
25        duplicate_architecture::DuplicateArchitecture,
26        invalid_spdx_license::NotSPDX,
27        long_values_aurweb::LongValuesAurweb,
28        no_architecture::NoArchitecture,
29        openpgp_key_id::OpenPGPKeyId,
30        undefined_architecture::UndefinedArchitecture,
31        unknown_architecture::UnknownArchitecture,
32        unsafe_checksum::UnsafeChecksum,
33    },
34};
35
36/// The data representation of a singular lint rule.
37///
38/// This is used to expose lints via the CLI so that the lints can be used in website generation or
39/// for development integration.
40#[cfg(feature = "serde")]
41#[derive(Clone, Debug, Serialize)]
42pub struct SerializableLintRule {
43    name: String,
44    scoped_name: String,
45    scope: LintScope,
46    level: Level,
47    groups: Vec<LintGroup>,
48    documentation: String,
49    option_names: Vec<String>,
50}
51
52/// The constructor function type that is used by each implementation of [`LintRule`].
53///
54/// E.g. [`DuplicateArchitecture::new_boxed`]. These constructors are saved in the [`LintStore`].
55type LintConstructor = fn(&LintRuleConfiguration) -> Box<dyn LintRule>;
56
57/// A map of lint rule name and generic [`LintRule`] implementations.
58///
59/// Used in [`LintStore`] to describe tuples of lint rule names and [`LintRule`] implementations.
60type LintMap = BTreeMap<String, Box<dyn LintRule>>;
61
62/// The [`LintStore`], which contains all available and known lint rules.
63///
64/// It can be used to further filter and select lints based on various criteria.
65pub struct LintStore {
66    config: LintConfiguration,
67    lint_constructors: Vec<LintConstructor>,
68    initialized_lints: LintMap,
69}
70
71impl fmt::Debug for LintStore {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("LintStore")
74            .field("config", &self.config)
75            .field("lint_constructors", &"Vec<LintConstructor>")
76            .field("initialized_lints", &"LintMap")
77            .finish()
78    }
79}
80
81impl LintStore {
82    /// Creates a new [`LintStore`].
83    ///
84    /// This adds all known lint rules to the store.
85    pub fn new(config: LintConfiguration) -> Self {
86        let mut store = Self {
87            config,
88            lint_constructors: Vec::new(),
89            initialized_lints: BTreeMap::new(),
90        };
91        store.register();
92        store.initialize_lint_rules();
93
94        store
95    }
96
97    /// Registers all lints that are made available in the store.
98    ///
99    /// # Note
100    ///
101    /// New lints must be specified in this function!
102    fn register(&mut self) {
103        // **IMPORTANT** NOTE: ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
104        // When you edit this, please sort the array while at it :)
105        // Much appreciated!
106        self.lint_constructors = vec![
107            DuplicateArchitecture::new_boxed,
108            LongValuesAurweb::new_boxed,
109            NoArchitecture::new_boxed,
110            NotSPDX::new_boxed,
111            OpenPGPKeyId::new_boxed,
112            UndefinedArchitecture::new_boxed,
113            UnknownArchitecture::new_boxed,
114            UnsafeChecksum::new_boxed,
115        ];
116    }
117
118    /// Initializes and configures all linting rules.
119    ///
120    /// This function instantly returns if the lints have already been initialized.
121    fn initialize_lint_rules(&mut self) {
122        // Early return if the lints are already initialized.
123        if !self.initialized_lints.is_empty() {
124            return;
125        }
126
127        for lint in &self.lint_constructors {
128            let initialized = lint(&self.config.options);
129
130            self.initialized_lints
131                .insert(initialized.scoped_name(), initialized);
132        }
133    }
134
135    /// Returns a reference to the map of all available and configured lint rules.
136    pub fn lint_rules(&self) -> &LintMap {
137        &self.initialized_lints
138    }
139
140    /// Returns a specific lint rule by its scoped name.
141    ///
142    /// Returns [`None`] if no lint rule with a matching `name` exists.
143    // False positive lint warning on the return type.
144    #[allow(clippy::borrowed_box)]
145    pub fn lint_rule_by_name(&self, name: &ScopedName) -> Option<&Box<dyn LintRule>> {
146        self.initialized_lints.get(&name.to_string())
147    }
148
149    /// Returns a map of all available and configured lint rules as [`SerializableLintRule`].
150    #[cfg(feature = "serde")]
151    pub fn serializable_lint_rules(&self) -> BTreeMap<String, SerializableLintRule> {
152        let mut map = BTreeMap::new();
153        for (scoped_name, lint) in &self.initialized_lints {
154            // Make sure that there's no duplicate key.
155            // We explicitly choose a `panic` as this is considered a hard consistency error.
156            //
157            // This is also covered by a test case, so it should really never happen in a release.
158            if map.contains_key(scoped_name) {
159                panic!("Encountered duplicate lint with name: {scoped_name}");
160            }
161
162            map.insert(
163                scoped_name.clone(),
164                SerializableLintRule {
165                    name: lint.name().to_string(),
166                    scoped_name: scoped_name.clone(),
167                    scope: lint.scope(),
168                    level: lint.level(),
169                    groups: lint.groups().to_vec(),
170                    documentation: lint.documentation().to_string(),
171                    option_names: lint
172                        .configuration_options()
173                        .iter()
174                        .map(LintRuleConfigurationOptionName::to_string)
175                        .collect(),
176                },
177            );
178        }
179
180        map
181    }
182
183    /// Returns lint rules that match a filter consisting of [`LintScope`] and [`Level`].
184    ///
185    /// This function filters out all lint rules that are not explicitly included **and**
186    /// - assigned to a deactivated group,
187    /// - **or** have a level above the max_level,
188    /// - **or** are explicitly ignored.
189    pub fn filtered_lint_rules<'a>(
190        &'a self,
191        scope: &LintScope,
192        max_level: Level,
193    ) -> FilteredLintRules<'a> {
194        FilteredLintRules::new(
195            &self.config,
196            self.initialized_lints.iter(),
197            *scope,
198            max_level,
199        )
200    }
201}
202
203/// The iterator that is returned by `LintConfiguration.initialized_lints.iter()`.
204type BTreeMapRuleIter<'a> = btree_map::Iter<'a, String, Box<dyn LintRule>>;
205
206/// An Iterator that allows iterating over lint rules filtered by a specific configuration file.
207///
208/// # Examples
209///
210/// ```
211/// use alpm_lint::{Level, LintScope, LintStore, config::LintConfiguration};
212///
213/// // Build a default config and use it to filter all lints from the store.
214/// let config = LintConfiguration::default();
215/// let store = LintStore::new(config);
216/// let mut iterator = store.filtered_lint_rules(&LintScope::SourceInfo, Level::Suggest);
217///
218/// // We get a lint
219/// assert!(iterator.next().is_some())
220/// ```
221pub struct FilteredLintRules<'a> {
222    /// The configuration used for filtering lint rules.
223    config: &'a LintConfiguration,
224    /// The unfiltered iterator over all lint rules.
225    rules_iter: BTreeMapRuleIter<'a>,
226    /// The scope in which lint rules should be.
227    scope: LintScope,
228    /// The lowest [`Level`] from which lint rules are considered.
229    min_level: Level,
230}
231
232impl<'a> FilteredLintRules<'a> {
233    /// Creates a new [`FilteredLintRules`].
234    pub fn new(
235        config: &'a LintConfiguration,
236        rules_iter: BTreeMapRuleIter<'a>,
237        scope: LintScope,
238        min_level: Level,
239    ) -> Self {
240        Self {
241            config,
242            rules_iter,
243            scope,
244            min_level,
245        }
246    }
247}
248
249impl std::fmt::Debug for FilteredLintRules<'_> {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        f.debug_struct("FilteredLintRules")
252            .field("config", &self.config)
253            .field("scope", &self.scope)
254            .field("min_level", &self.min_level)
255            .finish()
256    }
257}
258
259impl<'a> Iterator for FilteredLintRules<'a> {
260    type Item = (&'a String, &'a Box<dyn LintRule>);
261
262    // Allow while_let on an iterator. This pattern is required to give us more control
263    // over `self.rules_iter`.
264    #[allow(clippy::while_let_on_iterator)]
265    fn next(&mut self) -> Option<Self::Item> {
266        'outer: while let Some((name, rule)) = self.rules_iter.next() {
267            // Check whether this rule is explicitly disabled.
268            // If so immediately skip it.
269            if self.config.disabled_rules.contains(name) {
270                continue;
271            }
272
273            // Check whether this rule is explicitly enabled.
274            // If so immediately return it.
275            if self.config.enabled_rules.contains(name) {
276                return Some((name, rule));
277            }
278
279            // Skip any lint rules that're below the specified severity level threshold.
280            // The higher the number, the less important the Level.
281            // (e.g. Error=1, Suggest=4).
282            if rule.level() as isize > self.min_level as isize {
283                continue;
284            }
285
286            // If the groups are not empty, check whether all lint groups are enabled in the
287            // configuration file. If so, the lint will be returned, otherwise skip it.
288            let groups = rule.groups();
289            if !groups.is_empty() {
290                // As there are very few groups, an `n * m` lookup is reasonable.
291                for group in groups {
292                    if !self.config.groups.contains(group) {
293                        // A group isn't enabled, skip the rule.
294                        continue 'outer;
295                    }
296                }
297            }
298
299            // Make sure that the selected scope includes this specific lint rule.
300            let lint_rule_scope = rule.scope();
301            if !self.scope.contains(&lint_rule_scope) {
302                continue;
303            }
304
305            return Some((name, rule));
306        }
307
308        None
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    /// Unit tests for the LintStore itself
317    mod lint_store {
318        use std::collections::HashSet;
319
320        use alpm_lint_config::{LintConfiguration, LintRuleConfiguration};
321        use testresult::TestResult;
322
323        use super::LintStore;
324
325        /// Ensures that no two lint rules have the same scoped name.
326        ///
327        /// This is extremely important as to prevent naming conflicts and to ensure that each lint
328        /// rule has a unique identifier.
329        #[test]
330        fn no_duplicate_scoped_names() {
331            let store = LintStore::new(LintConfiguration::default());
332            let config = LintRuleConfiguration::default();
333
334            // Test the raw constructors for duplicate scoped names
335            let constructors = store.lint_constructors;
336            let mut scoped_names = HashSet::<String>::new();
337
338            for constructor in constructors {
339                let lint_rule = constructor(&config);
340                let scoped_name = lint_rule.scoped_name();
341
342                if scoped_names.contains(&scoped_name) {
343                    panic!("Found duplicate scoped lint rule name: {scoped_name}");
344                }
345                scoped_names.insert(scoped_name);
346            }
347        }
348
349        /// Ensures that all lint rule names only consist of lower-case alphanumerics or
350        /// underscores.
351        #[test]
352        fn lowercase_alphanum_underscore_names() -> TestResult {
353            let store = LintStore::new(LintConfiguration::default());
354            let config = LintRuleConfiguration::default();
355
356            for constructor in store.lint_constructors {
357                let lint_rule = constructor(&config);
358                let name = lint_rule.name();
359
360                let is_valid = name
361                    .chars()
362                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
363
364                if !is_valid {
365                    let scoped_name = lint_rule.scoped_name();
366                    panic!(
367                        "Found lint rule name with invalid character: '{scoped_name}'
368Lint rule names are only allowed to consist of lowercase alphanumeric characters and underscores."
369                    );
370                }
371            }
372
373            Ok(())
374        }
375    }
376
377    /// Tests for the the FilteredLintRules iterator
378    mod filtered_lint_rules {
379        use std::collections::BTreeMap;
380
381        use alpm_lint_config::{LintConfiguration, LintGroup};
382
383        use super::FilteredLintRules;
384        use crate::internal_prelude::*;
385        //
386        // The iterator is explicitly tested without the store as the store will always contain all
387        // lints, meaning that the list of tested lints might change over time.
388        //
389        // To isolate things a bit and to make testing deterministic, we create some "MockLintRule"s
390        // on which we perform the filtering.
391
392        /// Test implementation of [`LintRule`] for unit testing.
393        struct MockLintRule {
394            name: &'static str,
395            scope: LintScope,
396            level: Level,
397            groups: &'static [LintGroup],
398        }
399
400        impl LintRule for MockLintRule {
401            fn name(&self) -> &'static str {
402                self.name
403            }
404
405            fn scope(&self) -> LintScope {
406                self.scope
407            }
408
409            fn level(&self) -> Level {
410                self.level
411            }
412
413            fn groups(&self) -> &'static [LintGroup] {
414                self.groups
415            }
416
417            fn run(
418                &self,
419                _resources: &Resources,
420                _issues: &mut Vec<LintIssue>,
421            ) -> Result<(), Error> {
422                Ok(())
423            }
424
425            fn documentation(&self) -> String {
426                format!("Documentation for {}", self.name)
427            }
428
429            fn help_text(&self) -> String {
430                format!("Help for {}", self.name)
431            }
432        }
433
434        impl MockLintRule {
435            /// Creates a mock lint rules.
436            fn new_boxed(name: &'static str, scope: LintScope) -> Box<dyn LintRule> {
437                Box::new(Self {
438                    name,
439                    scope,
440                    level: Level::Warn,
441                    groups: &[],
442                })
443            }
444
445            /// Creates a mock lint rule with specified groups.
446            fn with_groups(
447                name: &'static str,
448                scope: LintScope,
449                groups: &'static [LintGroup],
450            ) -> Box<dyn LintRule> {
451                Box::new(Self {
452                    name,
453                    scope,
454                    level: Level::Warn,
455                    groups,
456                })
457            }
458
459            /// Creates a mock lint rule with a specific level.
460            fn with_level(name: &'static str, scope: LintScope, level: Level) -> Box<dyn LintRule> {
461                Box::new(Self {
462                    name,
463                    scope,
464                    level,
465                    groups: &[],
466                })
467            }
468        }
469
470        /// Helper function to assert the next rule name from a filtered iterator.
471        fn next_is(filtered: &mut FilteredLintRules, expected_name: &str) {
472            let (name, _) = filtered
473                .next()
474                .unwrap_or_else(|| panic!("Should have {expected_name}"));
475            assert_eq!(name, expected_name);
476        }
477
478        /// Helper function to assert that the filtered iterator has no more rules.
479        fn next_is_none(filtered: &mut FilteredLintRules) {
480            assert!(filtered.next().is_none(), "Should have no more rules");
481        }
482
483        /// Creates a set of mock lint rules for testing with differing properties.
484        fn create_mock_rules() -> BTreeMap<String, Box<dyn LintRule>> {
485            let mut rules = BTreeMap::new();
486
487            // Always enabled for SourceInfo
488            let rule1 = MockLintRule::new_boxed("test_rule_1", LintScope::SourceInfo);
489            // Always enabled for PackageBuild
490            let rule2 = MockLintRule::new_boxed("test_rule_2", LintScope::PackageBuild);
491            // Pedantic SourceInfo Rule
492            let rule3 = MockLintRule::with_groups(
493                "pedantic_rule",
494                LintScope::SourceInfo,
495                &[LintGroup::Pedantic],
496            );
497            // Testing Group SourceInfo Rule
498            let rule4 = MockLintRule::with_groups(
499                "testing_rule",
500                LintScope::SourceInfo,
501                &[LintGroup::Testing],
502            );
503            // Pedantic **and** Testing groups SourceInfo Rule
504            let rule5 = MockLintRule::with_groups(
505                "multi_group_rule",
506                LintScope::SourceInfo,
507                &[LintGroup::Pedantic, LintGroup::Testing],
508            );
509            let rule6 = MockLintRule::with_level("with_error", LintScope::SourceInfo, Level::Error);
510
511            rules.insert(rule1.scoped_name(), rule1);
512            rules.insert(rule2.scoped_name(), rule2);
513            rules.insert(rule3.scoped_name(), rule3);
514            rules.insert(rule4.scoped_name(), rule4);
515            rules.insert(rule5.scoped_name(), rule5);
516            rules.insert(rule6.scoped_name(), rule6);
517
518            rules
519        }
520
521        /// Ensures that filtering respects scope boundaries.
522        #[test]
523        fn filters_by_scope() {
524            let config = LintConfiguration::default();
525            let rules = create_mock_rules();
526            let mut filtered = FilteredLintRules::new(
527                &config,
528                rules.iter(),
529                LintScope::SourceInfo,
530                Level::Suggest,
531            );
532
533            // Should include only ungrouped SourceInfo scope rules
534            // test_rule_1 is the only rule that's by default enabled for the SourceInfo scope.
535            next_is(&mut filtered, "source_info::test_rule_1");
536            next_is(&mut filtered, "source_info::with_error");
537            next_is_none(&mut filtered);
538        }
539
540        /// Ensures that explicitly disabled rules are excluded.
541        #[test]
542        fn respects_disabled_rules() {
543            let config = LintConfiguration {
544                disabled_rules: vec![
545                    "source_info::test_rule_1".to_string(),
546                    "source_info::with_error".to_string(),
547                ],
548                ..Default::default()
549            };
550            let rules = create_mock_rules();
551            let mut filtered = FilteredLintRules::new(
552                &config,
553                rules.iter(),
554                LintScope::SourceInfo,
555                Level::Suggest,
556            );
557
558            // Should exclude the disabled rule.
559            next_is_none(&mut filtered);
560        }
561
562        /// Ensures that explicitly enabled rules bypass group filtering.
563        #[test]
564        fn includes_explicitly_enabled_rules() {
565            let config = LintConfiguration {
566                enabled_rules: vec!["source_info::pedantic_rule".to_string()],
567                groups: vec![], // No groups enabled
568                ..Default::default()
569            };
570            let rules = create_mock_rules();
571            let mut filtered = FilteredLintRules::new(
572                &config,
573                rules.iter(),
574                LintScope::SourceInfo,
575                Level::Suggest,
576            );
577
578            // Should include the explicitly enabled pedantic rule even with no groups
579            next_is(&mut filtered, "source_info::pedantic_rule");
580            next_is(&mut filtered, "source_info::test_rule_1");
581            next_is(&mut filtered, "source_info::with_error");
582            next_is_none(&mut filtered);
583        }
584
585        /// Ensures that disabling rules takes precedence over enabling rules.
586        #[test]
587        fn disabled_rules_take_precedence() {
588            let config = LintConfiguration {
589                disabled_rules: vec![
590                    "source_info::test_rule_1".to_string(),
591                    "source_info::with_error".to_string(),
592                ],
593                enabled_rules: vec!["source_info::test_rule_1".to_string()],
594                ..Default::default()
595            };
596            let rules = create_mock_rules();
597            let mut filtered = FilteredLintRules::new(
598                &config,
599                rules.iter(),
600                LintScope::SourceInfo,
601                Level::Suggest,
602            );
603
604            // Disabled rules are checked first and take precedence
605            next_is_none(&mut filtered);
606        }
607
608        /// Ensures that rules with multiple groups require *ALL* groups to be enabled.
609        #[test]
610        fn multi_group_requires_all_groups() {
611            let config = LintConfiguration {
612                groups: vec![LintGroup::Pedantic], // Only one group enabled
613                ..Default::default()
614            };
615            let rules = create_mock_rules();
616            let mut filtered = FilteredLintRules::new(
617                &config,
618                rules.iter(),
619                LintScope::SourceInfo,
620                Level::Suggest,
621            );
622
623            // Should get pedantic_rule and test_rule_1, but not multi_group_rule
624            next_is(&mut filtered, "source_info::pedantic_rule");
625            next_is(&mut filtered, "source_info::test_rule_1");
626            next_is(&mut filtered, "source_info::with_error");
627            next_is_none(&mut filtered);
628        }
629
630        /// Ensures that multi-group lint rules are included when all their groups are enabled.
631        #[test]
632        fn multi_group_included() {
633            let config = LintConfiguration {
634                groups: vec![LintGroup::Pedantic, LintGroup::Testing],
635                ..Default::default()
636            };
637            let rules = create_mock_rules();
638            let mut filtered = FilteredLintRules::new(
639                &config,
640                rules.iter(),
641                LintScope::SourceInfo,
642                Level::Suggest,
643            );
644
645            // Should get all SourceInfo rules: multi_group_rule, pedantic_rule, test_rule_1,
646            // testing_rule
647            next_is(&mut filtered, "source_info::multi_group_rule");
648            next_is(&mut filtered, "source_info::pedantic_rule");
649            next_is(&mut filtered, "source_info::test_rule_1");
650            next_is(&mut filtered, "source_info::testing_rule");
651            next_is(&mut filtered, "source_info::with_error");
652            next_is_none(&mut filtered);
653        }
654
655        /// Ensures that the scope hierarchy is respected in filtering.
656        #[test]
657        fn source_repository_scope() {
658            let config = LintConfiguration::default();
659            let rules = create_mock_rules();
660            let mut filtered = FilteredLintRules::new(
661                &config,
662                rules.iter(),
663                LintScope::SourceRepository,
664                Level::Suggest,
665            );
666
667            // SourceRepository scope should include both SourceInfo and PackageBuild rules
668            // Both test_rule_1 and test_rule_2 are ungrouped and match the scope
669            next_is(&mut filtered, "package_build::test_rule_2");
670            next_is(&mut filtered, "source_info::test_rule_1");
671            next_is(&mut filtered, "source_info::with_error");
672            next_is_none(&mut filtered);
673        }
674
675        /// Ensures that rules are filtered by minimum level threshold.
676        #[test]
677        fn filters_by_level() {
678            let config = LintConfiguration::default();
679            let rules = create_mock_rules();
680
681            // Test with Error level threshold
682            let mut filtered =
683                FilteredLintRules::new(&config, rules.iter(), LintScope::SourceInfo, Level::Error);
684            next_is(&mut filtered, "source_info::with_error");
685            next_is_none(&mut filtered);
686        }
687    }
688}