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 no_architecture::NoArchitecture,
23 openpgp_key_id::OpenPGPKeyId,
24 undefined_architecture::UndefinedArchitecture,
25 unsafe_checksum::UnsafeChecksum,
26 },
27};
28
29#[derive(Clone, Debug, Serialize)]
34pub struct SerializableLintRule {
35 name: String,
36 scoped_name: String,
37 scope: LintScope,
38 level: Level,
39 groups: Vec<LintGroup>,
40 documentation: String,
41 option_names: Vec<String>,
42}
43
44type LintConstructor = fn(&LintRuleConfiguration) -> Box<dyn LintRule>;
48
49type LintMap = BTreeMap<String, Box<dyn LintRule>>;
53
54pub struct LintStore {
58 config: LintConfiguration,
59 lint_constructors: Vec<LintConstructor>,
60 initialized_lints: LintMap,
61}
62
63impl fmt::Debug for LintStore {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.debug_struct("LintStore")
66 .field("config", &self.config)
67 .field("lint_constructors", &"Vec<LintConstructor>")
68 .field("initialized_lints", &"LintMap")
69 .finish()
70 }
71}
72
73impl LintStore {
74 pub fn new(config: LintConfiguration) -> Self {
78 let mut store = Self {
79 config,
80 lint_constructors: Vec::new(),
81 initialized_lints: BTreeMap::new(),
82 };
83 store.register();
84 store.initialize_lint_rules();
85
86 store
87 }
88
89 fn register(&mut self) {
95 self.lint_constructors = vec![
99 DuplicateArchitecture::new_boxed,
100 NoArchitecture::new_boxed,
101 NotSPDX::new_boxed,
102 OpenPGPKeyId::new_boxed,
103 UndefinedArchitecture::new_boxed,
104 UnsafeChecksum::new_boxed,
105 ];
106 }
107
108 fn initialize_lint_rules(&mut self) {
112 if !self.initialized_lints.is_empty() {
114 return;
115 }
116
117 for lint in &self.lint_constructors {
118 let initialized = lint(&self.config.options);
119
120 self.initialized_lints
121 .insert(initialized.scoped_name(), initialized);
122 }
123 }
124
125 pub fn lint_rules(&self) -> &LintMap {
127 &self.initialized_lints
128 }
129
130 #[allow(clippy::borrowed_box)]
135 pub fn lint_rule_by_name(&self, name: &ScopedName) -> Option<&Box<dyn LintRule>> {
136 self.initialized_lints.get(&name.to_string())
137 }
138
139 pub fn serializable_lint_rules(&self) -> BTreeMap<String, SerializableLintRule> {
141 let mut map = BTreeMap::new();
142 for (scoped_name, lint) in &self.initialized_lints {
143 if map.contains_key(scoped_name) {
148 panic!("Encountered duplicate lint with name: {scoped_name}");
149 }
150
151 map.insert(
152 scoped_name.clone(),
153 SerializableLintRule {
154 name: lint.name().to_string(),
155 scoped_name: scoped_name.clone(),
156 scope: lint.scope(),
157 level: lint.level(),
158 groups: lint.groups().to_vec(),
159 documentation: lint.documentation().to_string(),
160 option_names: lint
161 .configuration_options()
162 .iter()
163 .map(LintRuleConfigurationOptionName::to_string)
164 .collect(),
165 },
166 );
167 }
168
169 map
170 }
171
172 pub fn filtered_lint_rules<'a>(
179 &'a self,
180 scope: &LintScope,
181 max_level: Level,
182 ) -> FilteredLintRules<'a> {
183 FilteredLintRules::new(
184 &self.config,
185 self.initialized_lints.iter(),
186 *scope,
187 max_level,
188 )
189 }
190}
191
192type BTreeMapRuleIter<'a> = btree_map::Iter<'a, String, Box<dyn LintRule>>;
194
195pub struct FilteredLintRules<'a> {
211 config: &'a LintConfiguration,
213 rules_iter: BTreeMapRuleIter<'a>,
215 scope: LintScope,
217 min_level: Level,
219}
220
221impl<'a> FilteredLintRules<'a> {
222 pub fn new(
224 config: &'a LintConfiguration,
225 rules_iter: BTreeMapRuleIter<'a>,
226 scope: LintScope,
227 min_level: Level,
228 ) -> Self {
229 Self {
230 config,
231 rules_iter,
232 scope,
233 min_level,
234 }
235 }
236}
237
238impl std::fmt::Debug for FilteredLintRules<'_> {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 f.debug_struct("FilteredLintRules")
241 .field("config", &self.config)
242 .field("scope", &self.scope)
243 .field("min_level", &self.min_level)
244 .finish()
245 }
246}
247
248impl<'a> Iterator for FilteredLintRules<'a> {
249 type Item = (&'a String, &'a Box<dyn LintRule>);
250
251 #[allow(clippy::while_let_on_iterator)]
254 fn next(&mut self) -> Option<Self::Item> {
255 'outer: while let Some((name, rule)) = self.rules_iter.next() {
256 if self.config.disabled_rules.contains(name) {
259 continue;
260 }
261
262 if self.config.enabled_rules.contains(name) {
265 return Some((name, rule));
266 }
267
268 if rule.level() as isize > self.min_level as isize {
272 continue;
273 }
274
275 let groups = rule.groups();
278 if !groups.is_empty() {
279 for group in groups {
281 if !self.config.groups.contains(group) {
282 continue 'outer;
284 }
285 }
286 }
287
288 let lint_rule_scope = rule.scope();
290 if !self.scope.contains(&lint_rule_scope) {
291 continue;
292 }
293
294 return Some((name, rule));
295 }
296
297 None
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 mod lint_store {
307 use std::collections::HashSet;
308
309 use alpm_lint_config::{LintConfiguration, LintRuleConfiguration};
310 use testresult::TestResult;
311
312 use super::LintStore;
313
314 #[test]
319 fn no_duplicate_scoped_names() {
320 let store = LintStore::new(LintConfiguration::default());
321 let config = LintRuleConfiguration::default();
322
323 let constructors = store.lint_constructors;
325 let mut scoped_names = HashSet::<String>::new();
326
327 for constructor in constructors {
328 let lint_rule = constructor(&config);
329 let scoped_name = lint_rule.scoped_name();
330
331 if scoped_names.contains(&scoped_name) {
332 panic!("Found duplicate scoped lint rule name: {scoped_name}");
333 }
334 scoped_names.insert(scoped_name);
335 }
336 }
337
338 #[test]
341 fn lowercase_alphanum_underscore_names() -> TestResult {
342 let store = LintStore::new(LintConfiguration::default());
343 let config = LintRuleConfiguration::default();
344
345 for constructor in store.lint_constructors {
346 let lint_rule = constructor(&config);
347 let name = lint_rule.name();
348
349 let is_valid = name
350 .chars()
351 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
352
353 if !is_valid {
354 let scoped_name = lint_rule.scoped_name();
355 return Err(format!(
356 "Found lint rule name with invalid character: '{scoped_name}'
357Lint rule names are only allowed to consist of lowercase alphanumeric characters and underscores."
358 )
359 .into());
360 }
361 }
362
363 Ok(())
364 }
365 }
366
367 mod filtered_lint_rules {
369 use std::collections::BTreeMap;
370
371 use alpm_lint_config::{LintConfiguration, LintGroup};
372
373 use super::FilteredLintRules;
374 use crate::internal_prelude::*;
375 struct MockLintRule {
384 name: &'static str,
385 scope: LintScope,
386 level: Level,
387 groups: &'static [LintGroup],
388 }
389
390 impl LintRule for MockLintRule {
391 fn name(&self) -> &'static str {
392 self.name
393 }
394
395 fn scope(&self) -> LintScope {
396 self.scope
397 }
398
399 fn level(&self) -> Level {
400 self.level
401 }
402
403 fn groups(&self) -> &'static [LintGroup] {
404 self.groups
405 }
406
407 fn run(
408 &self,
409 _resources: &Resources,
410 _issues: &mut Vec<LintIssue>,
411 ) -> Result<(), Error> {
412 Ok(())
413 }
414
415 fn documentation(&self) -> String {
416 format!("Documentation for {}", self.name)
417 }
418
419 fn help_text(&self) -> String {
420 format!("Help for {}", self.name)
421 }
422 }
423
424 impl MockLintRule {
425 fn new_boxed(name: &'static str, scope: LintScope) -> Box<dyn LintRule> {
427 Box::new(Self {
428 name,
429 scope,
430 level: Level::Warn,
431 groups: &[],
432 })
433 }
434
435 fn with_groups(
437 name: &'static str,
438 scope: LintScope,
439 groups: &'static [LintGroup],
440 ) -> Box<dyn LintRule> {
441 Box::new(Self {
442 name,
443 scope,
444 level: Level::Warn,
445 groups,
446 })
447 }
448
449 fn with_level(name: &'static str, scope: LintScope, level: Level) -> Box<dyn LintRule> {
451 Box::new(Self {
452 name,
453 scope,
454 level,
455 groups: &[],
456 })
457 }
458 }
459
460 fn next_is(filtered: &mut FilteredLintRules, expected_name: &str) {
462 let (name, _) = filtered
463 .next()
464 .unwrap_or_else(|| panic!("Should have {expected_name}"));
465 assert_eq!(name, expected_name);
466 }
467
468 fn next_is_none(filtered: &mut FilteredLintRules) {
470 assert!(filtered.next().is_none(), "Should have no more rules");
471 }
472
473 fn create_mock_rules() -> BTreeMap<String, Box<dyn LintRule>> {
475 let mut rules = BTreeMap::new();
476
477 let rule1 = MockLintRule::new_boxed("test_rule_1", LintScope::SourceInfo);
479 let rule2 = MockLintRule::new_boxed("test_rule_2", LintScope::PackageBuild);
481 let rule3 = MockLintRule::with_groups(
483 "pedantic_rule",
484 LintScope::SourceInfo,
485 &[LintGroup::Pedantic],
486 );
487 let rule4 = MockLintRule::with_groups(
489 "testing_rule",
490 LintScope::SourceInfo,
491 &[LintGroup::Testing],
492 );
493 let rule5 = MockLintRule::with_groups(
495 "multi_group_rule",
496 LintScope::SourceInfo,
497 &[LintGroup::Pedantic, LintGroup::Testing],
498 );
499 let rule6 = MockLintRule::with_level("with_error", LintScope::SourceInfo, Level::Error);
500
501 rules.insert(rule1.scoped_name(), rule1);
502 rules.insert(rule2.scoped_name(), rule2);
503 rules.insert(rule3.scoped_name(), rule3);
504 rules.insert(rule4.scoped_name(), rule4);
505 rules.insert(rule5.scoped_name(), rule5);
506 rules.insert(rule6.scoped_name(), rule6);
507
508 rules
509 }
510
511 #[test]
513 fn filters_by_scope() {
514 let config = LintConfiguration::default();
515 let rules = create_mock_rules();
516 let mut filtered = FilteredLintRules::new(
517 &config,
518 rules.iter(),
519 LintScope::SourceInfo,
520 Level::Suggest,
521 );
522
523 next_is(&mut filtered, "source_info::test_rule_1");
526 next_is(&mut filtered, "source_info::with_error");
527 next_is_none(&mut filtered);
528 }
529
530 #[test]
532 fn respects_disabled_rules() {
533 let config = LintConfiguration {
534 disabled_rules: vec![
535 "source_info::test_rule_1".to_string(),
536 "source_info::with_error".to_string(),
537 ],
538 ..Default::default()
539 };
540 let rules = create_mock_rules();
541 let mut filtered = FilteredLintRules::new(
542 &config,
543 rules.iter(),
544 LintScope::SourceInfo,
545 Level::Suggest,
546 );
547
548 next_is_none(&mut filtered);
550 }
551
552 #[test]
554 fn includes_explicitly_enabled_rules() {
555 let config = LintConfiguration {
556 enabled_rules: vec!["source_info::pedantic_rule".to_string()],
557 groups: vec![], ..Default::default()
559 };
560 let rules = create_mock_rules();
561 let mut filtered = FilteredLintRules::new(
562 &config,
563 rules.iter(),
564 LintScope::SourceInfo,
565 Level::Suggest,
566 );
567
568 next_is(&mut filtered, "source_info::pedantic_rule");
570 next_is(&mut filtered, "source_info::test_rule_1");
571 next_is(&mut filtered, "source_info::with_error");
572 next_is_none(&mut filtered);
573 }
574
575 #[test]
577 fn disabled_rules_take_precedence() {
578 let config = LintConfiguration {
579 disabled_rules: vec![
580 "source_info::test_rule_1".to_string(),
581 "source_info::with_error".to_string(),
582 ],
583 enabled_rules: vec!["source_info::test_rule_1".to_string()],
584 ..Default::default()
585 };
586 let rules = create_mock_rules();
587 let mut filtered = FilteredLintRules::new(
588 &config,
589 rules.iter(),
590 LintScope::SourceInfo,
591 Level::Suggest,
592 );
593
594 next_is_none(&mut filtered);
596 }
597
598 #[test]
600 fn multi_group_requires_all_groups() {
601 let config = LintConfiguration {
602 groups: vec![LintGroup::Pedantic], ..Default::default()
604 };
605 let rules = create_mock_rules();
606 let mut filtered = FilteredLintRules::new(
607 &config,
608 rules.iter(),
609 LintScope::SourceInfo,
610 Level::Suggest,
611 );
612
613 next_is(&mut filtered, "source_info::pedantic_rule");
615 next_is(&mut filtered, "source_info::test_rule_1");
616 next_is(&mut filtered, "source_info::with_error");
617 next_is_none(&mut filtered);
618 }
619
620 #[test]
622 fn multi_group_included() {
623 let config = LintConfiguration {
624 groups: vec![LintGroup::Pedantic, LintGroup::Testing],
625 ..Default::default()
626 };
627 let rules = create_mock_rules();
628 let mut filtered = FilteredLintRules::new(
629 &config,
630 rules.iter(),
631 LintScope::SourceInfo,
632 Level::Suggest,
633 );
634
635 next_is(&mut filtered, "source_info::multi_group_rule");
638 next_is(&mut filtered, "source_info::pedantic_rule");
639 next_is(&mut filtered, "source_info::test_rule_1");
640 next_is(&mut filtered, "source_info::testing_rule");
641 next_is(&mut filtered, "source_info::with_error");
642 next_is_none(&mut filtered);
643 }
644
645 #[test]
647 fn source_repository_scope() {
648 let config = LintConfiguration::default();
649 let rules = create_mock_rules();
650 let mut filtered = FilteredLintRules::new(
651 &config,
652 rules.iter(),
653 LintScope::SourceRepository,
654 Level::Suggest,
655 );
656
657 next_is(&mut filtered, "package_build::test_rule_2");
660 next_is(&mut filtered, "source_info::test_rule_1");
661 next_is(&mut filtered, "source_info::with_error");
662 next_is_none(&mut filtered);
663 }
664
665 #[test]
667 fn filters_by_level() {
668 let config = LintConfiguration::default();
669 let rules = create_mock_rules();
670
671 let mut filtered =
673 FilteredLintRules::new(&config, rules.iter(), LintScope::SourceInfo, Level::Error);
674 next_is(&mut filtered, "source_info::with_error");
675 next_is_none(&mut filtered);
676 }
677 }
678}