1use 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#[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
52type LintConstructor = fn(&LintRuleConfiguration) -> Box<dyn LintRule>;
56
57type LintMap = BTreeMap<String, Box<dyn LintRule>>;
61
62pub 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 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 fn register(&mut self) {
103 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 fn initialize_lint_rules(&mut self) {
122 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 pub fn lint_rules(&self) -> &LintMap {
137 &self.initialized_lints
138 }
139
140 #[expect(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 #[cfg(feature = "serde")]
151 #[expect(clippy::panic)]
152 pub fn serializable_lint_rules(&self) -> BTreeMap<String, SerializableLintRule> {
153 let mut map = BTreeMap::new();
154 for (scoped_name, lint) in &self.initialized_lints {
155 if map.contains_key(scoped_name) {
160 panic!("Encountered duplicate lint with name: {scoped_name}");
161 }
162
163 map.insert(
164 scoped_name.clone(),
165 SerializableLintRule {
166 name: lint.name().to_string(),
167 scoped_name: scoped_name.clone(),
168 scope: lint.scope(),
169 level: lint.level(),
170 groups: lint.groups().to_vec(),
171 documentation: lint.documentation().to_string(),
172 option_names: lint
173 .configuration_options()
174 .iter()
175 .map(LintRuleConfigurationOptionName::to_string)
176 .collect(),
177 },
178 );
179 }
180
181 map
182 }
183
184 pub fn filtered_lint_rules<'a>(
191 &'a self,
192 scope: &LintScope,
193 max_level: Level,
194 ) -> FilteredLintRules<'a> {
195 FilteredLintRules::new(
196 &self.config,
197 self.initialized_lints.iter(),
198 *scope,
199 max_level,
200 )
201 }
202}
203
204type BTreeMapRuleIter<'a> = btree_map::Iter<'a, String, Box<dyn LintRule>>;
206
207pub struct FilteredLintRules<'a> {
223 config: &'a LintConfiguration,
225 rules_iter: BTreeMapRuleIter<'a>,
227 scope: LintScope,
229 min_level: Level,
231}
232
233impl<'a> FilteredLintRules<'a> {
234 pub fn new(
236 config: &'a LintConfiguration,
237 rules_iter: BTreeMapRuleIter<'a>,
238 scope: LintScope,
239 min_level: Level,
240 ) -> Self {
241 Self {
242 config,
243 rules_iter,
244 scope,
245 min_level,
246 }
247 }
248}
249
250impl std::fmt::Debug for FilteredLintRules<'_> {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("FilteredLintRules")
253 .field("config", &self.config)
254 .field("scope", &self.scope)
255 .field("min_level", &self.min_level)
256 .finish()
257 }
258}
259
260impl<'a> Iterator for FilteredLintRules<'a> {
261 type Item = (&'a String, &'a Box<dyn LintRule>);
262
263 #[expect(clippy::while_let_on_iterator)]
266 fn next(&mut self) -> Option<Self::Item> {
267 'outer: while let Some((name, rule)) = self.rules_iter.next() {
268 if self.config.disabled_rules.contains(name) {
271 continue;
272 }
273
274 if self.config.enabled_rules.contains(name) {
277 return Some((name, rule));
278 }
279
280 if rule.level() as isize > self.min_level as isize {
284 continue;
285 }
286
287 let groups = rule.groups();
290 if !groups.is_empty() {
291 for group in groups {
293 if !self.config.groups.contains(group) {
294 continue 'outer;
296 }
297 }
298 }
299
300 let lint_rule_scope = rule.scope();
302 if !self.scope.contains(&lint_rule_scope) {
303 continue;
304 }
305
306 return Some((name, rule));
307 }
308
309 None
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 mod lint_store {
319 use std::collections::HashSet;
320
321 use alpm_lint_config::{LintConfiguration, LintRuleConfiguration};
322 use testresult::TestResult;
323
324 use super::LintStore;
325
326 #[test]
331 fn no_duplicate_scoped_names() {
332 let store = LintStore::new(LintConfiguration::default());
333 let config = LintRuleConfiguration::default();
334
335 let constructors = store.lint_constructors;
337 let mut scoped_names = HashSet::<String>::new();
338
339 for constructor in constructors {
340 let lint_rule = constructor(&config);
341 let scoped_name = lint_rule.scoped_name();
342
343 if scoped_names.contains(&scoped_name) {
344 panic!("Found duplicate scoped lint rule name: {scoped_name}");
345 }
346 scoped_names.insert(scoped_name);
347 }
348 }
349
350 #[test]
353 fn lowercase_alphanum_underscore_names() -> TestResult {
354 let store = LintStore::new(LintConfiguration::default());
355 let config = LintRuleConfiguration::default();
356
357 for constructor in store.lint_constructors {
358 let lint_rule = constructor(&config);
359 let name = lint_rule.name();
360
361 let is_valid = name
362 .chars()
363 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
364
365 if !is_valid {
366 let scoped_name = lint_rule.scoped_name();
367 panic!(
368 "Found lint rule name with invalid character: '{scoped_name}'
369Lint rule names are only allowed to consist of lowercase alphanumeric characters and underscores."
370 );
371 }
372 }
373
374 Ok(())
375 }
376 }
377
378 mod filtered_lint_rules {
380 use std::collections::BTreeMap;
381
382 use alpm_lint_config::{LintConfiguration, LintGroup};
383
384 use super::FilteredLintRules;
385 use crate::internal_prelude::*;
386 struct MockLintRule {
395 name: &'static str,
396 scope: LintScope,
397 level: Level,
398 groups: &'static [LintGroup],
399 }
400
401 impl LintRule for MockLintRule {
402 fn name(&self) -> &'static str {
403 self.name
404 }
405
406 fn scope(&self) -> LintScope {
407 self.scope
408 }
409
410 fn level(&self) -> Level {
411 self.level
412 }
413
414 fn groups(&self) -> &'static [LintGroup] {
415 self.groups
416 }
417
418 fn run(
419 &self,
420 _resources: &Resources,
421 _issues: &mut Vec<LintIssue>,
422 ) -> Result<(), Error> {
423 Ok(())
424 }
425
426 fn documentation(&self) -> String {
427 format!("Documentation for {}", self.name)
428 }
429
430 fn help_text(&self) -> String {
431 format!("Help for {}", self.name)
432 }
433 }
434
435 impl MockLintRule {
436 fn new_boxed(name: &'static str, scope: LintScope) -> Box<dyn LintRule> {
438 Box::new(Self {
439 name,
440 scope,
441 level: Level::Warn,
442 groups: &[],
443 })
444 }
445
446 fn with_groups(
448 name: &'static str,
449 scope: LintScope,
450 groups: &'static [LintGroup],
451 ) -> Box<dyn LintRule> {
452 Box::new(Self {
453 name,
454 scope,
455 level: Level::Warn,
456 groups,
457 })
458 }
459
460 fn with_level(name: &'static str, scope: LintScope, level: Level) -> Box<dyn LintRule> {
462 Box::new(Self {
463 name,
464 scope,
465 level,
466 groups: &[],
467 })
468 }
469 }
470
471 fn next_is(filtered: &mut FilteredLintRules, expected_name: &str) {
473 let (name, _) = filtered
474 .next()
475 .unwrap_or_else(|| panic!("Should have {expected_name}"));
476 assert_eq!(name, expected_name);
477 }
478
479 fn next_is_none(filtered: &mut FilteredLintRules) {
481 assert!(filtered.next().is_none(), "Should have no more rules");
482 }
483
484 fn create_mock_rules() -> BTreeMap<String, Box<dyn LintRule>> {
486 let mut rules = BTreeMap::new();
487
488 let rule1 = MockLintRule::new_boxed("test_rule_1", LintScope::SourceInfo);
490 let rule2 = MockLintRule::new_boxed("test_rule_2", LintScope::PackageBuild);
492 let rule3 = MockLintRule::with_groups(
494 "pedantic_rule",
495 LintScope::SourceInfo,
496 &[LintGroup::Pedantic],
497 );
498 let rule4 = MockLintRule::with_groups(
500 "testing_rule",
501 LintScope::SourceInfo,
502 &[LintGroup::Testing],
503 );
504 let rule5 = MockLintRule::with_groups(
506 "multi_group_rule",
507 LintScope::SourceInfo,
508 &[LintGroup::Pedantic, LintGroup::Testing],
509 );
510 let rule6 = MockLintRule::with_level("with_error", LintScope::SourceInfo, Level::Error);
511
512 rules.insert(rule1.scoped_name(), rule1);
513 rules.insert(rule2.scoped_name(), rule2);
514 rules.insert(rule3.scoped_name(), rule3);
515 rules.insert(rule4.scoped_name(), rule4);
516 rules.insert(rule5.scoped_name(), rule5);
517 rules.insert(rule6.scoped_name(), rule6);
518
519 rules
520 }
521
522 #[test]
524 fn filters_by_scope() {
525 let config = LintConfiguration::default();
526 let rules = create_mock_rules();
527 let mut filtered = FilteredLintRules::new(
528 &config,
529 rules.iter(),
530 LintScope::SourceInfo,
531 Level::Suggest,
532 );
533
534 next_is(&mut filtered, "source_info::test_rule_1");
537 next_is(&mut filtered, "source_info::with_error");
538 next_is_none(&mut filtered);
539 }
540
541 #[test]
543 fn respects_disabled_rules() {
544 let config = LintConfiguration {
545 disabled_rules: vec![
546 "source_info::test_rule_1".to_string(),
547 "source_info::with_error".to_string(),
548 ],
549 ..Default::default()
550 };
551 let rules = create_mock_rules();
552 let mut filtered = FilteredLintRules::new(
553 &config,
554 rules.iter(),
555 LintScope::SourceInfo,
556 Level::Suggest,
557 );
558
559 next_is_none(&mut filtered);
561 }
562
563 #[test]
565 fn includes_explicitly_enabled_rules() {
566 let config = LintConfiguration {
567 enabled_rules: vec!["source_info::pedantic_rule".to_string()],
568 groups: vec![], ..Default::default()
570 };
571 let rules = create_mock_rules();
572 let mut filtered = FilteredLintRules::new(
573 &config,
574 rules.iter(),
575 LintScope::SourceInfo,
576 Level::Suggest,
577 );
578
579 next_is(&mut filtered, "source_info::pedantic_rule");
581 next_is(&mut filtered, "source_info::test_rule_1");
582 next_is(&mut filtered, "source_info::with_error");
583 next_is_none(&mut filtered);
584 }
585
586 #[test]
588 fn disabled_rules_take_precedence() {
589 let config = LintConfiguration {
590 disabled_rules: vec![
591 "source_info::test_rule_1".to_string(),
592 "source_info::with_error".to_string(),
593 ],
594 enabled_rules: vec!["source_info::test_rule_1".to_string()],
595 ..Default::default()
596 };
597 let rules = create_mock_rules();
598 let mut filtered = FilteredLintRules::new(
599 &config,
600 rules.iter(),
601 LintScope::SourceInfo,
602 Level::Suggest,
603 );
604
605 next_is_none(&mut filtered);
607 }
608
609 #[test]
611 fn multi_group_requires_all_groups() {
612 let config = LintConfiguration {
613 groups: vec![LintGroup::Pedantic], ..Default::default()
615 };
616 let rules = create_mock_rules();
617 let mut filtered = FilteredLintRules::new(
618 &config,
619 rules.iter(),
620 LintScope::SourceInfo,
621 Level::Suggest,
622 );
623
624 next_is(&mut filtered, "source_info::pedantic_rule");
626 next_is(&mut filtered, "source_info::test_rule_1");
627 next_is(&mut filtered, "source_info::with_error");
628 next_is_none(&mut filtered);
629 }
630
631 #[test]
633 fn multi_group_included() {
634 let config = LintConfiguration {
635 groups: vec![LintGroup::Pedantic, LintGroup::Testing],
636 ..Default::default()
637 };
638 let rules = create_mock_rules();
639 let mut filtered = FilteredLintRules::new(
640 &config,
641 rules.iter(),
642 LintScope::SourceInfo,
643 Level::Suggest,
644 );
645
646 next_is(&mut filtered, "source_info::multi_group_rule");
649 next_is(&mut filtered, "source_info::pedantic_rule");
650 next_is(&mut filtered, "source_info::test_rule_1");
651 next_is(&mut filtered, "source_info::testing_rule");
652 next_is(&mut filtered, "source_info::with_error");
653 next_is_none(&mut filtered);
654 }
655
656 #[test]
658 fn source_repository_scope() {
659 let config = LintConfiguration::default();
660 let rules = create_mock_rules();
661 let mut filtered = FilteredLintRules::new(
662 &config,
663 rules.iter(),
664 LintScope::SourceRepository,
665 Level::Suggest,
666 );
667
668 next_is(&mut filtered, "package_build::test_rule_2");
671 next_is(&mut filtered, "source_info::test_rule_1");
672 next_is(&mut filtered, "source_info::with_error");
673 next_is_none(&mut filtered);
674 }
675
676 #[test]
678 fn filters_by_level() {
679 let config = LintConfiguration::default();
680 let rules = create_mock_rules();
681
682 let mut filtered =
684 FilteredLintRules::new(&config, rules.iter(), LintScope::SourceInfo, Level::Error);
685 next_is(&mut filtered, "source_info::with_error");
686 next_is_none(&mut filtered);
687 }
688 }
689}