slopguard_core/rule/
load.rs1use std::collections::HashSet;
5use std::fs::read_to_string;
6use std::io::Error as IoError;
7use std::path::{Component, Path, PathBuf};
8use std::str::from_utf8;
9
10use ignore::WalkBuilder;
11use slopguard_rules::BuiltinRules;
12
13use super::{parse_rule, Category, Language, Rule, RuleError, RuleId};
14use crate::config::Config;
15use crate::source::{ResolvedSource, RuleOrigin};
16
17pub fn read_fixture(rule: &Rule, relative: &str) -> Result<String, RuleError> {
21 let path = Path::new(relative);
22 let traverses = path.components().any(|c| matches!(c, Component::ParentDir));
23 if path.is_absolute() || traverses {
24 return Err(RuleError::InvalidFixturePath {
25 id: rule.id.clone(),
26 path: relative.to_string(),
27 });
28 }
29
30 match &rule.source_dir {
31 Some(dir) => {
32 let full = dir.join(path);
33 read_to_string(&full).map_err(|e| {
34 if e.kind() == std::io::ErrorKind::NotFound {
35 RuleError::FixtureNotFound {
36 id: rule.id.clone(),
37 path: relative.to_string(),
38 }
39 } else {
40 RuleError::io(&full, e)
41 }
42 })
43 }
44 None => {
45 let file = BuiltinRules::get(relative).ok_or_else(|| RuleError::FixtureNotFound {
46 id: rule.id.clone(),
47 path: relative.to_string(),
48 })?;
49 from_utf8(&file.data)
50 .map(str::to_string)
51 .map_err(|e| RuleError::Utf8 {
52 path: relative.to_string(),
53 source: e,
54 })
55 }
56 }
57}
58
59fn parse_rule_at(yaml: &str, relative_path: &Path) -> Result<Rule, RuleError> {
62 let mut rule = parse_rule(yaml)?;
63 if rule.category.is_none() {
64 rule.category = Category::from_rule_path(relative_path);
65 }
66 Ok(rule)
67}
68
69pub(super) fn is_rule_file(path: &Path) -> bool {
70 matches!(
71 path.extension().and_then(|e| e.to_str()),
72 Some("yml" | "yaml")
73 )
74}
75
76pub fn load_builtin_rules() -> Result<Vec<Rule>, RuleError> {
78 BuiltinRules::iter()
79 .filter(|path| is_rule_file(Path::new(path.as_ref())))
80 .filter_map(|path| BuiltinRules::get(&path).map(|file| (path, file)))
81 .map(|(path, file)| {
82 let yaml = from_utf8(&file.data).map_err(|e| RuleError::Utf8 {
83 path: path.to_string(),
84 source: e,
85 })?;
86 parse_rule_at(yaml, Path::new(path.as_ref()))
87 })
88 .collect()
89}
90
91fn load_rules_in_dir(dir: &Path, origin: &RuleOrigin) -> Result<Vec<Rule>, RuleError> {
95 let mut rules = Vec::new();
96 for entry in WalkBuilder::new(dir).standard_filters(false).build() {
99 let entry = entry.map_err(|e| RuleError::io(dir, IoError::other(e)))?;
100 let path = entry.path();
101 let is_file = entry.file_type().is_some_and(|t| t.is_file());
102 if !is_file || !is_rule_file(path) {
103 continue;
104 }
105 let yaml = read_to_string(path).map_err(|e| RuleError::io(path, e))?;
106 let relative = path.strip_prefix(dir).unwrap_or(path);
107 let mut rule = parse_rule_at(&yaml, relative)?;
108 rule.source_dir = Some(dir.to_path_buf());
109 rule.origin = origin.clone();
110 rules.push(rule);
111 }
112 Ok(rules)
113}
114
115pub fn load_custom_rules(dirs: &[PathBuf]) -> Result<Vec<Rule>, RuleError> {
118 let mut rules = Vec::new();
119 for dir in dirs.iter().filter(|dir| dir.is_dir()) {
120 let origin = RuleOrigin::Local { path: dir.clone() };
121 rules.extend(load_rules_in_dir(dir, &origin)?);
122 }
123 Ok(rules)
124}
125
126pub fn load_source_rules(sources: &[ResolvedSource]) -> Result<Vec<Rule>, RuleError> {
130 let mut rules = Vec::new();
131 for source in sources.iter().filter(|s| s.dir.is_dir()) {
132 rules.extend(load_rules_in_dir(&source.dir, &source.origin)?);
133 }
134 Ok(rules)
135}
136
137fn reject_builtin_id_reuse(external: &[Rule], builtin: &[Rule]) -> Result<(), RuleError> {
140 let builtin_ids: HashSet<(&RuleId, &Language)> =
141 builtin.iter().map(|r| (&r.id, &r.language)).collect();
142 for rule in external {
143 if builtin_ids.contains(&(&rule.id, &rule.language)) {
144 return Err(RuleError::SourceReusesBuiltinId {
145 id: rule.id.clone(),
146 origin: rule.origin.label(),
147 });
148 }
149 }
150 Ok(())
151}
152
153pub fn is_rule_active(rule: &Rule, config: &Config) -> bool {
159 let listed = |ids: &[String]| ids.iter().any(|id| id == rule.id.as_str());
160 if listed(&config.rules.enable) {
161 return true;
162 }
163 let ruleset_on = match rule.effective_category() {
164 Category::Slop => config.rulesets.slop,
165 Category::Security => config.rulesets.security,
166 Category::Correctness => config.rulesets.correctness,
167 };
168 ruleset_on && rule.enabled && !listed(&config.rules.disable)
169}
170
171fn load_external_rules(
176 config: &Config,
177 sources: &[ResolvedSource],
178 builtin: &[Rule],
179) -> Result<(Vec<Rule>, Vec<Rule>), RuleError> {
180 let custom = load_custom_rules(&config.rules.custom_dirs)?;
181 let from_sources = load_source_rules(sources)?;
182 reject_builtin_id_reuse(&custom, builtin)?;
183 reject_builtin_id_reuse(&from_sources, builtin)?;
184 Ok((custom, from_sources))
185}
186
187pub fn load_effective_rules(
193 config: &Config,
194 sources: &[ResolvedSource],
195) -> Result<Vec<Rule>, RuleError> {
196 let builtin = load_builtin_rules()?;
197 let (custom, from_sources) = load_external_rules(config, sources, &builtin)?;
198
199 let mut rules: Vec<Rule> = builtin
202 .into_iter()
203 .chain(from_sources)
204 .filter(|rule| is_rule_active(rule, config))
205 .collect();
206 rules.extend(custom);
207 validate_unique_ids(&rules)?;
208 Ok(rules)
209}
210
211pub fn load_all_rules(config: &Config, sources: &[ResolvedSource]) -> Result<Vec<Rule>, RuleError> {
215 let builtin = load_builtin_rules()?;
216 let (custom, from_sources) = load_external_rules(config, sources, &builtin)?;
217
218 let mut rules = builtin;
219 rules.extend(custom);
220 rules.extend(from_sources);
221 Ok(rules)
222}
223
224pub fn validate_unique_ids(rules: &[Rule]) -> Result<(), RuleError> {
228 let mut seen = HashSet::new();
229 for rule in rules {
230 if !seen.insert((&rule.id, &rule.language)) {
231 return Err(RuleError::DuplicateId {
232 id: rule.id.clone(),
233 });
234 }
235 }
236 Ok(())
237}