1use std::{
9 collections::{BTreeMap, btree_map},
10 fmt,
11};
12
13use alpm_lint_config::{LintConfiguration, LintRuleConfiguration, LintRuleConfigurationOptionName};
14use serde::Serialize;
15
16use crate::{
17 ScopedName,
18 internal_prelude::{Level, LintGroup, LintRule, LintScope},
19 lint_rules::source_info::{
20 duplicate_architecture::DuplicateArchitecture,
21 invalid_spdx_license::NotSPDX,
22 long_values_aurweb::LongValuesAurweb,
23 no_architecture::NoArchitecture,
24 openpgp_key_id::OpenPGPKeyId,
25 undefined_architecture::UndefinedArchitecture,
26 unknown_architecture::UnknownArchitecture,
27 unsafe_checksum::UnsafeChecksum,
28 },
29};
30
31#[derive(Clone, Debug, Serialize)]
36pub struct SerializableLintRule {
37 name: String,
38 scoped_name: String,
39 scope: LintScope,
40 level: Level,
41 groups: Vec<LintGroup>,
42 documentation: String,
43 option_names: Vec<String>,
44}
45
46type LintConstructor = fn(&LintRuleConfiguration) -> Box<dyn LintRule>;
50
51type LintMap = BTreeMap<String, Box<dyn LintRule>>;
55
56pub struct LintStore {
60 config: LintConfiguration,
61 lint_constructors: Vec<LintConstructor>,
62 initialized_lints: LintMap,
63}
64
65impl fmt::Debug for LintStore {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.debug_struct("LintStore")
68 .field("config", &self.config)
69 .field("lint_constructors", &"Vec<LintConstructor>")
70 .field("initialized_lints", &"LintMap")
71 .finish()
72 }
73}
74
75impl LintStore {
76 pub fn new(config: LintConfiguration) -> Self {
80 let mut store = Self {
81 config,
82 lint_constructors: Vec::new(),
83 initialized_lints: BTreeMap::new(),
84 };
85 store.register();
86 store.initialize_lint_rules();
87
88 store
89 }
90
91 fn register(&mut self) {
97 self.lint_constructors = vec![
101 DuplicateArchitecture::new_boxed,
102 LongValuesAurweb::new_boxed,
103 NoArchitecture::new_boxed,
104 NotSPDX::new_boxed,
105 OpenPGPKeyId::new_boxed,
106 UndefinedArchitecture::new_boxed,
107 UnknownArchitecture::new_boxed,
108 UnsafeChecksum::new_boxed,
109 ];
110 }
111
112 fn initialize_lint_rules(&mut self) {
116 if !self.initialized_lints.is_empty() {
118 return;
119 }
120
121 for lint in &self.lint_constructors {
122 let initialized = lint(&self.config.options);
123
124 self.initialized_lints
125 .insert(initialized.scoped_name(), initialized);
126 }
127 }
128
129 pub fn lint_rules(&self) -> &LintMap {
131 &self.initialized_lints
132 }
133
134 #[allow(clippy::borrowed_box)]
139 pub fn lint_rule_by_name(&self, name: &ScopedName) -> Option<&Box<dyn LintRule>> {
140 self.initialized_lints.get(&name.to_string())
141 }
142
143 pub fn serializable_lint_rules(&self) -> BTreeMap<String, SerializableLintRule> {
145 let mut map = BTreeMap::new();
146 for (scoped_name, lint) in &self.initialized_lints {
147 if map.contains_key(scoped_name) {
152 panic!("Encountered duplicate lint with name: {scoped_name}");
153 }
154
155 map.insert(
156 scoped_name.clone(),
157 SerializableLintRule {
158 name: lint.name().to_string(),
159 scoped_name: scoped_name.clone(),
160 scope: lint.scope(),
161 level: lint.level(),
162 groups: lint.groups().to_vec(),
163 documentation: lint.documentation().to_string(),
164 option_names: lint
165 .configuration_options()
166 .iter()
167 .map(LintRuleConfigurationOptionName::to_string)
168 .collect(),
169 },
170 );
171 }
172
173 map
174 }
175
176 pub fn filtered_lint_rules<'a>(
183 &'a self,
184 scope: &LintScope,
185 max_level: Level,
186 ) -> FilteredLintRules<'a> {
187 FilteredLintRules::new(
188 &self.config,
189 self.initialized_lints.iter(),
190 *scope,
191 max_level,
192 )
193 }
194}
195
196type BTreeMapRuleIter<'a> = btree_map::Iter<'a, String, Box<dyn LintRule>>;
198
199pub struct FilteredLintRules<'a> {
215 config: &'a LintConfiguration,
217 rules_iter: BTreeMapRuleIter<'a>,
219 scope: LintScope,
221 min_level: Level,
223}
224
225impl<'a> FilteredLintRules<'a> {
226 pub fn new(
228 config: &'a LintConfiguration,
229 rules_iter: BTreeMapRuleIter<'a>,
230 scope: LintScope,
231 min_level: Level,
232 ) -> Self {
233 Self {
234 config,
235 rules_iter,
236 scope,
237 min_level,
238 }
239 }
240}
241
242impl std::fmt::Debug for FilteredLintRules<'_> {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 f.debug_struct("FilteredLintRules")
245 .field("config", &self.config)
246 .field("scope", &self.scope)
247 .field("min_level", &self.min_level)
248 .finish()
249 }
250}
251
252impl<'a> Iterator for FilteredLintRules<'a> {
253 type Item = (&'a String, &'a Box<dyn LintRule>);
254
255 #[allow(clippy::while_let_on_iterator)]
258 fn next(&mut self) -> Option<Self::Item> {
259 'outer: while let Some((name, rule)) = self.rules_iter.next() {
260 if self.config.disabled_rules.contains(name) {
263 continue;
264 }
265
266 if self.config.enabled_rules.contains(name) {
269 return Some((name, rule));
270 }
271
272 if rule.level() as isize > self.min_level as isize {
276 continue;
277 }
278
279 let groups = rule.groups();
282 if !groups.is_empty() {
283 for group in groups {
285 if !self.config.groups.contains(group) {
286 continue 'outer;
288 }
289 }
290 }
291
292 let lint_rule_scope = rule.scope();
294 if !self.scope.contains(&lint_rule_scope) {
295 continue;
296 }
297
298 return Some((name, rule));
299 }
300
301 None
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 mod lint_store {
311 use std::collections::HashSet;
312
313 use alpm_lint_config::{LintConfiguration, LintRuleConfiguration};
314 use testresult::TestResult;
315
316 use super::LintStore;
317
318 #[test]
323 fn no_duplicate_scoped_names() {
324 let store = LintStore::new(LintConfiguration::default());
325 let config = LintRuleConfiguration::default();
326
327 let constructors = store.lint_constructors;
329 let mut scoped_names = HashSet::<String>::new();
330
331 for constructor in constructors {
332 let lint_rule = constructor(&config);
333 let scoped_name = lint_rule.scoped_name();
334
335 if scoped_names.contains(&scoped_name) {
336 panic!("Found duplicate scoped lint rule name: {scoped_name}");
337 }
338 scoped_names.insert(scoped_name);
339 }
340 }
341
342 #[test]
345 fn lowercase_alphanum_underscore_names() -> TestResult {
346 let store = LintStore::new(LintConfiguration::default());
347 let config = LintRuleConfiguration::default();
348
349 for constructor in store.lint_constructors {
350 let lint_rule = constructor(&config);
351 let name = lint_rule.name();
352
353 let is_valid = name
354 .chars()
355 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
356
357 if !is_valid {
358 let scoped_name = lint_rule.scoped_name();
359 panic!(
360 "Found lint rule name with invalid character: '{scoped_name}'
361Lint rule names are only allowed to consist of lowercase alphanumeric characters and underscores."
362 );
363 }
364 }
365
366 Ok(())
367 }
368 }
369
370 mod filtered_lint_rules {
372 use std::collections::BTreeMap;
373
374 use alpm_lint_config::{LintConfiguration, LintGroup};
375
376 use super::FilteredLintRules;
377 use crate::internal_prelude::*;
378 struct MockLintRule {
387 name: &'static str,
388 scope: LintScope,
389 level: Level,
390 groups: &'static [LintGroup],
391 }
392
393 impl LintRule for MockLintRule {
394 fn name(&self) -> &'static str {
395 self.name
396 }
397
398 fn scope(&self) -> LintScope {
399 self.scope
400 }
401
402 fn level(&self) -> Level {
403 self.level
404 }
405
406 fn groups(&self) -> &'static [LintGroup] {
407 self.groups
408 }
409
410 fn run(
411 &self,
412 _resources: &Resources,
413 _issues: &mut Vec<LintIssue>,
414 ) -> Result<(), Error> {
415 Ok(())
416 }
417
418 fn documentation(&self) -> String {
419 format!("Documentation for {}", self.name)
420 }
421
422 fn help_text(&self) -> String {
423 format!("Help for {}", self.name)
424 }
425 }
426
427 impl MockLintRule {
428 fn new_boxed(name: &'static str, scope: LintScope) -> Box<dyn LintRule> {
430 Box::new(Self {
431 name,
432 scope,
433 level: Level::Warn,
434 groups: &[],
435 })
436 }
437
438 fn with_groups(
440 name: &'static str,
441 scope: LintScope,
442 groups: &'static [LintGroup],
443 ) -> Box<dyn LintRule> {
444 Box::new(Self {
445 name,
446 scope,
447 level: Level::Warn,
448 groups,
449 })
450 }
451
452 fn with_level(name: &'static str, scope: LintScope, level: Level) -> Box<dyn LintRule> {
454 Box::new(Self {
455 name,
456 scope,
457 level,
458 groups: &[],
459 })
460 }
461 }
462
463 fn next_is(filtered: &mut FilteredLintRules, expected_name: &str) {
465 let (name, _) = filtered
466 .next()
467 .unwrap_or_else(|| panic!("Should have {expected_name}"));
468 assert_eq!(name, expected_name);
469 }
470
471 fn next_is_none(filtered: &mut FilteredLintRules) {
473 assert!(filtered.next().is_none(), "Should have no more rules");
474 }
475
476 fn create_mock_rules() -> BTreeMap<String, Box<dyn LintRule>> {
478 let mut rules = BTreeMap::new();
479
480 let rule1 = MockLintRule::new_boxed("test_rule_1", LintScope::SourceInfo);
482 let rule2 = MockLintRule::new_boxed("test_rule_2", LintScope::PackageBuild);
484 let rule3 = MockLintRule::with_groups(
486 "pedantic_rule",
487 LintScope::SourceInfo,
488 &[LintGroup::Pedantic],
489 );
490 let rule4 = MockLintRule::with_groups(
492 "testing_rule",
493 LintScope::SourceInfo,
494 &[LintGroup::Testing],
495 );
496 let rule5 = MockLintRule::with_groups(
498 "multi_group_rule",
499 LintScope::SourceInfo,
500 &[LintGroup::Pedantic, LintGroup::Testing],
501 );
502 let rule6 = MockLintRule::with_level("with_error", LintScope::SourceInfo, Level::Error);
503
504 rules.insert(rule1.scoped_name(), rule1);
505 rules.insert(rule2.scoped_name(), rule2);
506 rules.insert(rule3.scoped_name(), rule3);
507 rules.insert(rule4.scoped_name(), rule4);
508 rules.insert(rule5.scoped_name(), rule5);
509 rules.insert(rule6.scoped_name(), rule6);
510
511 rules
512 }
513
514 #[test]
516 fn filters_by_scope() {
517 let config = LintConfiguration::default();
518 let rules = create_mock_rules();
519 let mut filtered = FilteredLintRules::new(
520 &config,
521 rules.iter(),
522 LintScope::SourceInfo,
523 Level::Suggest,
524 );
525
526 next_is(&mut filtered, "source_info::test_rule_1");
529 next_is(&mut filtered, "source_info::with_error");
530 next_is_none(&mut filtered);
531 }
532
533 #[test]
535 fn respects_disabled_rules() {
536 let config = LintConfiguration {
537 disabled_rules: vec![
538 "source_info::test_rule_1".to_string(),
539 "source_info::with_error".to_string(),
540 ],
541 ..Default::default()
542 };
543 let rules = create_mock_rules();
544 let mut filtered = FilteredLintRules::new(
545 &config,
546 rules.iter(),
547 LintScope::SourceInfo,
548 Level::Suggest,
549 );
550
551 next_is_none(&mut filtered);
553 }
554
555 #[test]
557 fn includes_explicitly_enabled_rules() {
558 let config = LintConfiguration {
559 enabled_rules: vec!["source_info::pedantic_rule".to_string()],
560 groups: vec![], ..Default::default()
562 };
563 let rules = create_mock_rules();
564 let mut filtered = FilteredLintRules::new(
565 &config,
566 rules.iter(),
567 LintScope::SourceInfo,
568 Level::Suggest,
569 );
570
571 next_is(&mut filtered, "source_info::pedantic_rule");
573 next_is(&mut filtered, "source_info::test_rule_1");
574 next_is(&mut filtered, "source_info::with_error");
575 next_is_none(&mut filtered);
576 }
577
578 #[test]
580 fn disabled_rules_take_precedence() {
581 let config = LintConfiguration {
582 disabled_rules: vec![
583 "source_info::test_rule_1".to_string(),
584 "source_info::with_error".to_string(),
585 ],
586 enabled_rules: vec!["source_info::test_rule_1".to_string()],
587 ..Default::default()
588 };
589 let rules = create_mock_rules();
590 let mut filtered = FilteredLintRules::new(
591 &config,
592 rules.iter(),
593 LintScope::SourceInfo,
594 Level::Suggest,
595 );
596
597 next_is_none(&mut filtered);
599 }
600
601 #[test]
603 fn multi_group_requires_all_groups() {
604 let config = LintConfiguration {
605 groups: vec![LintGroup::Pedantic], ..Default::default()
607 };
608 let rules = create_mock_rules();
609 let mut filtered = FilteredLintRules::new(
610 &config,
611 rules.iter(),
612 LintScope::SourceInfo,
613 Level::Suggest,
614 );
615
616 next_is(&mut filtered, "source_info::pedantic_rule");
618 next_is(&mut filtered, "source_info::test_rule_1");
619 next_is(&mut filtered, "source_info::with_error");
620 next_is_none(&mut filtered);
621 }
622
623 #[test]
625 fn multi_group_included() {
626 let config = LintConfiguration {
627 groups: vec![LintGroup::Pedantic, LintGroup::Testing],
628 ..Default::default()
629 };
630 let rules = create_mock_rules();
631 let mut filtered = FilteredLintRules::new(
632 &config,
633 rules.iter(),
634 LintScope::SourceInfo,
635 Level::Suggest,
636 );
637
638 next_is(&mut filtered, "source_info::multi_group_rule");
641 next_is(&mut filtered, "source_info::pedantic_rule");
642 next_is(&mut filtered, "source_info::test_rule_1");
643 next_is(&mut filtered, "source_info::testing_rule");
644 next_is(&mut filtered, "source_info::with_error");
645 next_is_none(&mut filtered);
646 }
647
648 #[test]
650 fn source_repository_scope() {
651 let config = LintConfiguration::default();
652 let rules = create_mock_rules();
653 let mut filtered = FilteredLintRules::new(
654 &config,
655 rules.iter(),
656 LintScope::SourceRepository,
657 Level::Suggest,
658 );
659
660 next_is(&mut filtered, "package_build::test_rule_2");
663 next_is(&mut filtered, "source_info::test_rule_1");
664 next_is(&mut filtered, "source_info::with_error");
665 next_is_none(&mut filtered);
666 }
667
668 #[test]
670 fn filters_by_level() {
671 let config = LintConfiguration::default();
672 let rules = create_mock_rules();
673
674 let mut filtered =
676 FilteredLintRules::new(&config, rules.iter(), LintScope::SourceInfo, Level::Error);
677 next_is(&mut filtered, "source_info::with_error");
678 next_is_none(&mut filtered);
679 }
680 }
681}