1use super::*;
2
3pub struct LintConfigBuilder {
24 severity: Option<SeverityLevel>,
25 rules: Option<RuleConfig>,
26 exclude: Option<Vec<String>>,
27 target: Option<TargetTool>,
28 tools: Option<Vec<String>>,
29 mcp_protocol_version: Option<Option<String>>,
30 tool_versions: Option<ToolVersions>,
31 spec_revisions: Option<SpecRevisions>,
32 files: Option<FilesConfig>,
33 locale: Option<Option<String>>,
34 max_files_to_validate: Option<Option<usize>>,
35 root_dir: Option<PathBuf>,
37 import_cache: Option<crate::parsers::ImportCache>,
38 fs: Option<Arc<dyn FileSystem>>,
39 disabled_rules: Vec<String>,
40 disabled_validators: Vec<String>,
41}
42
43impl LintConfigBuilder {
44 fn append_and_dedup(target: &mut Vec<String>, source: &mut Vec<String>) {
45 if source.is_empty() {
46 return;
47 }
48
49 target.append(source);
50 let mut seen = std::collections::HashSet::new();
51 target.retain(|item| seen.insert(item.clone()));
52 }
53
54 fn new() -> Self {
58 Self {
59 severity: None,
60 rules: None,
61 exclude: None,
62 target: None,
63 tools: None,
64 mcp_protocol_version: None,
65 tool_versions: None,
66 spec_revisions: None,
67 files: None,
68 locale: None,
69 max_files_to_validate: None,
70 root_dir: None,
71 import_cache: None,
72 fs: None,
73 disabled_rules: Vec::new(),
74 disabled_validators: Vec::new(),
75 }
76 }
77
78 pub fn severity(&mut self, severity: SeverityLevel) -> &mut Self {
80 self.severity = Some(severity);
81 self
82 }
83
84 pub fn rules(&mut self, rules: RuleConfig) -> &mut Self {
86 self.rules = Some(rules);
87 self
88 }
89
90 pub fn exclude(&mut self, exclude: Vec<String>) -> &mut Self {
92 self.exclude = Some(exclude);
93 self
94 }
95
96 pub fn target(&mut self, target: TargetTool) -> &mut Self {
98 self.target = Some(target);
99 self
100 }
101
102 pub fn tools(&mut self, tools: Vec<String>) -> &mut Self {
104 self.tools = Some(tools);
105 self
106 }
107
108 pub fn mcp_protocol_version(&mut self, version: Option<String>) -> &mut Self {
110 self.mcp_protocol_version = Some(version);
111 self
112 }
113
114 pub fn tool_versions(&mut self, versions: ToolVersions) -> &mut Self {
116 self.tool_versions = Some(versions);
117 self
118 }
119
120 pub fn spec_revisions(&mut self, revisions: SpecRevisions) -> &mut Self {
122 self.spec_revisions = Some(revisions);
123 self
124 }
125
126 pub fn files(&mut self, files: FilesConfig) -> &mut Self {
128 self.files = Some(files);
129 self
130 }
131
132 pub fn locale(&mut self, locale: Option<String>) -> &mut Self {
134 self.locale = Some(locale);
135 self
136 }
137
138 pub fn max_files_to_validate(&mut self, max: Option<usize>) -> &mut Self {
140 self.max_files_to_validate = Some(max);
141 self
142 }
143
144 pub fn root_dir(&mut self, root_dir: PathBuf) -> &mut Self {
146 self.root_dir = Some(root_dir);
147 self
148 }
149
150 pub fn import_cache(&mut self, cache: crate::parsers::ImportCache) -> &mut Self {
152 self.import_cache = Some(cache);
153 self
154 }
155
156 pub fn fs(&mut self, fs: Arc<dyn FileSystem>) -> &mut Self {
158 self.fs = Some(fs);
159 self
160 }
161
162 pub fn disable_rule(&mut self, rule_id: impl Into<String>) -> &mut Self {
164 self.disabled_rules.push(rule_id.into());
165 self
166 }
167
168 pub fn disable_validator(&mut self, name: impl Into<String>) -> &mut Self {
170 self.disabled_validators.push(name.into());
171 self
172 }
173
174 pub fn build(&mut self) -> Result<LintConfig, ConfigError> {
182 let config = self.build_inner();
183
184 Self::validate_patterns(&config)?;
185
186 let warnings = config.validate();
188 if !warnings.is_empty() {
189 return Err(ConfigError::ValidationFailed(warnings));
190 }
191
192 Ok(config)
193 }
194
195 pub fn build_lenient(&mut self) -> Result<LintConfig, ConfigError> {
203 let config = self.build_inner();
204 Self::validate_patterns(&config)?;
205 Ok(config)
206 }
207
208 #[cfg(any(test, feature = "__internal_unchecked"))]
214 #[doc(hidden)]
215 pub fn build_unchecked(&mut self) -> LintConfig {
216 self.build_inner()
217 }
218
219 fn validate_patterns(config: &LintConfig) -> Result<(), ConfigError> {
223 let pattern_lists: &[(&str, &[String])] = &[
224 ("exclude", &config.data.exclude),
225 (
226 "files.include_as_memory",
227 &config.data.files.include_as_memory,
228 ),
229 (
230 "files.include_as_generic",
231 &config.data.files.include_as_generic,
232 ),
233 ("files.exclude", &config.data.files.exclude),
234 ];
235 for &(field, patterns) in pattern_lists {
236 for pattern in patterns {
237 let normalized = pattern.replace('\\', "/");
238 if let Err(e) = glob::Pattern::new(&normalized) {
239 return Err(ConfigError::InvalidGlobPattern {
240 pattern: pattern.clone(),
241 error: format!("{} (in {})", e, field),
242 });
243 }
244 if has_path_traversal(&normalized) {
245 return Err(ConfigError::PathTraversal {
246 pattern: pattern.clone(),
247 });
248 }
249 if normalized.starts_with('/')
250 || (normalized.len() >= 3
251 && normalized.as_bytes()[0].is_ascii_alphabetic()
252 && normalized.as_bytes().get(1..3) == Some(b":/"))
253 {
254 return Err(ConfigError::AbsolutePathPattern {
255 pattern: pattern.clone(),
256 });
257 }
258 }
259 }
260 Ok(())
261 }
262
263 fn build_inner(&mut self) -> LintConfig {
265 let defaults = ConfigData::default();
266
267 let mut rules = self.rules.take().unwrap_or(defaults.rules);
268
269 Self::append_and_dedup(&mut rules.disabled_rules, &mut self.disabled_rules);
271 Self::append_and_dedup(
272 &mut rules.disabled_validators,
273 &mut self.disabled_validators,
274 );
275
276 let config_data = ConfigData {
277 severity: self.severity.take().unwrap_or(defaults.severity),
278 rules,
279 exclude: self.exclude.take().unwrap_or(defaults.exclude),
280 target: self.target.take().unwrap_or(defaults.target),
281 tools: self.tools.take().unwrap_or(defaults.tools),
282 mcp_protocol_version: self
283 .mcp_protocol_version
284 .take()
285 .unwrap_or(defaults.mcp_protocol_version),
286 tool_versions: self.tool_versions.take().unwrap_or(defaults.tool_versions),
287 spec_revisions: self
288 .spec_revisions
289 .take()
290 .unwrap_or(defaults.spec_revisions),
291 files: self.files.take().unwrap_or(defaults.files),
292 locale: self.locale.take().unwrap_or(defaults.locale),
293 max_files_to_validate: self
294 .max_files_to_validate
295 .take()
296 .unwrap_or(defaults.max_files_to_validate),
297 };
298
299 let mut config = LintConfig {
300 data: Arc::new(config_data),
301 runtime: RuntimeContext::default(),
302 };
303
304 if let Some(root_dir) = self.root_dir.take() {
306 config.runtime.root_dir = Some(root_dir);
307 }
308 if let Some(cache) = self.import_cache.take() {
309 config.runtime.import_cache = Some(cache);
310 }
311 if let Some(fs) = self.fs.take() {
312 config.runtime.fs = fs;
313 }
314
315 config
316 }
317}
318
319impl Default for LintConfigBuilder {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325impl LintConfig {
326 pub fn builder() -> LintConfigBuilder {
328 LintConfigBuilder::new()
329 }
330}