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