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 #[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 #[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 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 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
203type BTreeMapRuleIter<'a> = btree_map::Iter<'a, String, Box<dyn LintRule>>;
205
206pub struct FilteredLintRules<'a> {
222 config: &'a LintConfiguration,
224 rules_iter: BTreeMapRuleIter<'a>,
226 scope: LintScope,
228 min_level: Level,
230}
231
232impl<'a> FilteredLintRules<'a> {
233 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(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 if self.config.disabled_rules.contains(name) {
270 continue;
271 }
272
273 if self.config.enabled_rules.contains(name) {
276 return Some((name, rule));
277 }
278
279 if rule.level() as isize > self.min_level as isize {
283 continue;
284 }
285
286 let groups = rule.groups();
289 if !groups.is_empty() {
290 for group in groups {
292 if !self.config.groups.contains(group) {
293 continue 'outer;
295 }
296 }
297 }
298
299 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 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 #[test]
330 fn no_duplicate_scoped_names() {
331 let store = LintStore::new(LintConfiguration::default());
332 let config = LintRuleConfiguration::default();
333
334 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 #[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 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 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 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 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 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 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 fn next_is_none(filtered: &mut FilteredLintRules) {
480 assert!(filtered.next().is_none(), "Should have no more rules");
481 }
482
483 fn create_mock_rules() -> BTreeMap<String, Box<dyn LintRule>> {
485 let mut rules = BTreeMap::new();
486
487 let rule1 = MockLintRule::new_boxed("test_rule_1", LintScope::SourceInfo);
489 let rule2 = MockLintRule::new_boxed("test_rule_2", LintScope::PackageBuild);
491 let rule3 = MockLintRule::with_groups(
493 "pedantic_rule",
494 LintScope::SourceInfo,
495 &[LintGroup::Pedantic],
496 );
497 let rule4 = MockLintRule::with_groups(
499 "testing_rule",
500 LintScope::SourceInfo,
501 &[LintGroup::Testing],
502 );
503 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 #[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 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 #[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 next_is_none(&mut filtered);
560 }
561
562 #[test]
564 fn includes_explicitly_enabled_rules() {
565 let config = LintConfiguration {
566 enabled_rules: vec!["source_info::pedantic_rule".to_string()],
567 groups: vec![], ..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 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 #[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 next_is_none(&mut filtered);
606 }
607
608 #[test]
610 fn multi_group_requires_all_groups() {
611 let config = LintConfiguration {
612 groups: vec![LintGroup::Pedantic], ..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 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 #[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 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 #[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 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 #[test]
677 fn filters_by_level() {
678 let config = LintConfiguration::default();
679 let rules = create_mock_rules();
680
681 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}