solidity_language_server/
config.rs1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone)]
8pub struct FoundryConfig {
9 pub root: PathBuf,
11 pub solc_version: Option<String>,
14 pub remappings: Vec<String>,
17 pub via_ir: bool,
20 pub optimizer: bool,
22 pub optimizer_runs: u64,
25 pub evm_version: Option<String>,
29 pub ignored_error_codes: Vec<u64>,
31}
32
33impl Default for FoundryConfig {
34 fn default() -> Self {
35 Self {
36 root: PathBuf::new(),
37 solc_version: None,
38 remappings: Vec::new(),
39 via_ir: false,
40 optimizer: false,
41 optimizer_runs: 200,
42 evm_version: None,
43 ignored_error_codes: Vec::new(),
44 }
45 }
46}
47
48pub fn load_foundry_config(file_path: &Path) -> FoundryConfig {
50 let toml_path = match find_foundry_toml(file_path) {
51 Some(p) => p,
52 None => return FoundryConfig::default(),
53 };
54 load_foundry_config_from_toml(&toml_path)
55}
56
57pub fn load_foundry_config_from_toml(toml_path: &Path) -> FoundryConfig {
59 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
60
61 let content = match std::fs::read_to_string(toml_path) {
62 Ok(c) => c,
63 Err(_) => {
64 return FoundryConfig {
65 root,
66 ..Default::default()
67 };
68 }
69 };
70
71 let table: toml::Table = match content.parse() {
72 Ok(t) => t,
73 Err(_) => {
74 return FoundryConfig {
75 root,
76 ..Default::default()
77 };
78 }
79 };
80
81 let profile_name = std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string());
82
83 let profile = table
84 .get("profile")
85 .and_then(|p| p.as_table())
86 .and_then(|p| p.get(&profile_name))
87 .and_then(|p| p.as_table());
88
89 let profile = match profile {
90 Some(p) => p,
91 None => {
92 return FoundryConfig {
93 root,
94 ..Default::default()
95 };
96 }
97 };
98
99 let solc_version = profile
101 .get("solc")
102 .or_else(|| profile.get("solc_version"))
103 .and_then(|v| v.as_str())
104 .map(|s| s.to_string());
105
106 let remappings = profile
108 .get("remappings")
109 .and_then(|v| v.as_array())
110 .map(|arr| {
111 arr.iter()
112 .filter_map(|v| v.as_str())
113 .map(|s| s.to_string())
114 .collect()
115 })
116 .unwrap_or_default();
117
118 let via_ir = profile
120 .get("via_ir")
121 .and_then(|v| v.as_bool())
122 .unwrap_or(false);
123
124 let optimizer = profile
126 .get("optimizer")
127 .and_then(|v| v.as_bool())
128 .unwrap_or(false);
129
130 let optimizer_runs = profile
132 .get("optimizer_runs")
133 .and_then(|v| v.as_integer())
134 .map(|v| v as u64)
135 .unwrap_or(200);
136
137 let evm_version = profile
139 .get("evm_version")
140 .and_then(|v| v.as_str())
141 .map(|s| s.to_string());
142
143 let ignored_error_codes = profile
145 .get("ignored_error_codes")
146 .and_then(|v| v.as_array())
147 .map(|arr| {
148 arr.iter()
149 .filter_map(|v| v.as_integer())
150 .map(|v| v as u64)
151 .collect()
152 })
153 .unwrap_or_default();
154
155 FoundryConfig {
156 root,
157 solc_version,
158 remappings,
159 via_ir,
160 optimizer,
161 optimizer_runs,
162 evm_version,
163 ignored_error_codes,
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct LintConfig {
170 pub root: PathBuf,
172 pub lint_on_build: bool,
174 pub ignore_patterns: Vec<glob::Pattern>,
176}
177
178impl Default for LintConfig {
179 fn default() -> Self {
180 Self {
181 root: PathBuf::new(),
182 lint_on_build: true,
183 ignore_patterns: Vec::new(),
184 }
185 }
186}
187
188impl LintConfig {
189 pub fn should_lint(&self, file_path: &Path) -> bool {
195 if !self.lint_on_build {
196 return false;
197 }
198
199 if self.ignore_patterns.is_empty() {
200 return true;
201 }
202
203 let relative = file_path.strip_prefix(&self.root).unwrap_or(file_path);
206
207 let rel_str = relative.to_string_lossy();
208
209 for pattern in &self.ignore_patterns {
210 if pattern.matches(&rel_str) {
211 return false;
212 }
213 }
214
215 true
216 }
217}
218
219fn find_git_root(start: &Path) -> Option<PathBuf> {
224 let start = if start.is_file() {
225 start.parent()?
226 } else {
227 start
228 };
229 start
230 .ancestors()
231 .find(|p| p.join(".git").exists())
232 .map(Path::to_path_buf)
233}
234
235pub fn find_foundry_toml(start: &Path) -> Option<PathBuf> {
240 let start_dir = if start.is_file() {
241 start.parent()?
242 } else {
243 start
244 };
245
246 let boundary = find_git_root(start_dir);
247
248 start_dir
249 .ancestors()
250 .take_while(|p| {
252 if let Some(boundary) = &boundary {
253 p.starts_with(boundary)
254 } else {
255 true
256 }
257 })
258 .find(|p| p.join("foundry.toml").is_file())
259 .map(|p| p.join("foundry.toml"))
260}
261
262pub fn load_lint_config(file_path: &Path) -> LintConfig {
266 let toml_path = match find_foundry_toml(file_path) {
267 Some(p) => p,
268 None => return LintConfig::default(),
269 };
270
271 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
272
273 let content = match std::fs::read_to_string(&toml_path) {
274 Ok(c) => c,
275 Err(_) => {
276 return LintConfig {
277 root,
278 ..Default::default()
279 };
280 }
281 };
282
283 let table: toml::Table = match content.parse() {
284 Ok(t) => t,
285 Err(_) => {
286 return LintConfig {
287 root,
288 ..Default::default()
289 };
290 }
291 };
292
293 let profile_name = std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string());
295
296 let lint_table = table
298 .get("profile")
299 .and_then(|p| p.as_table())
300 .and_then(|p| p.get(&profile_name))
301 .and_then(|p| p.as_table())
302 .and_then(|p| p.get("lint"))
303 .and_then(|l| l.as_table());
304
305 let lint_table = match lint_table {
306 Some(t) => t,
307 None => {
308 return LintConfig {
309 root,
310 ..Default::default()
311 };
312 }
313 };
314
315 let lint_on_build = lint_table
317 .get("lint_on_build")
318 .and_then(|v| v.as_bool())
319 .unwrap_or(true);
320
321 let ignore_patterns = lint_table
323 .get("ignore")
324 .and_then(|v| v.as_array())
325 .map(|arr| {
326 arr.iter()
327 .filter_map(|v| v.as_str())
328 .filter_map(|s| glob::Pattern::new(s).ok())
329 .collect()
330 })
331 .unwrap_or_default();
332
333 LintConfig {
334 root,
335 lint_on_build,
336 ignore_patterns,
337 }
338}
339
340pub fn load_lint_config_from_toml(toml_path: &Path) -> LintConfig {
343 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
344
345 let content = match std::fs::read_to_string(toml_path) {
346 Ok(c) => c,
347 Err(_) => {
348 return LintConfig {
349 root,
350 ..Default::default()
351 };
352 }
353 };
354
355 let table: toml::Table = match content.parse() {
356 Ok(t) => t,
357 Err(_) => {
358 return LintConfig {
359 root,
360 ..Default::default()
361 };
362 }
363 };
364
365 let profile_name = std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string());
366
367 let lint_table = table
368 .get("profile")
369 .and_then(|p| p.as_table())
370 .and_then(|p| p.get(&profile_name))
371 .and_then(|p| p.as_table())
372 .and_then(|p| p.get("lint"))
373 .and_then(|l| l.as_table());
374
375 let lint_table = match lint_table {
376 Some(t) => t,
377 None => {
378 return LintConfig {
379 root,
380 ..Default::default()
381 };
382 }
383 };
384
385 let lint_on_build = lint_table
386 .get("lint_on_build")
387 .and_then(|v| v.as_bool())
388 .unwrap_or(true);
389
390 let ignore_patterns = lint_table
391 .get("ignore")
392 .and_then(|v| v.as_array())
393 .map(|arr| {
394 arr.iter()
395 .filter_map(|v| v.as_str())
396 .filter_map(|s| glob::Pattern::new(s).ok())
397 .collect()
398 })
399 .unwrap_or_default();
400
401 LintConfig {
402 root,
403 lint_on_build,
404 ignore_patterns,
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use std::fs;
412
413 #[test]
414 fn test_default_config_lints_everything() {
415 let config = LintConfig::default();
416 assert!(config.should_lint(Path::new("test/MyTest.sol")));
417 assert!(config.should_lint(Path::new("src/Token.sol")));
418 }
419
420 #[test]
421 fn test_lint_on_build_false_skips_all() {
422 let config = LintConfig {
423 lint_on_build: false,
424 ..Default::default()
425 };
426 assert!(!config.should_lint(Path::new("src/Token.sol")));
427 }
428
429 #[test]
430 fn test_ignore_pattern_matches() {
431 let config = LintConfig {
432 root: PathBuf::from("/project"),
433 lint_on_build: true,
434 ignore_patterns: vec![glob::Pattern::new("test/**/*").unwrap()],
435 };
436 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
437 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
438 }
439
440 #[test]
441 fn test_multiple_ignore_patterns() {
442 let config = LintConfig {
443 root: PathBuf::from("/project"),
444 lint_on_build: true,
445 ignore_patterns: vec![
446 glob::Pattern::new("test/**/*").unwrap(),
447 glob::Pattern::new("script/**/*").unwrap(),
448 ],
449 };
450 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
451 assert!(!config.should_lint(Path::new("/project/script/Deploy.sol")));
452 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
453 }
454
455 #[test]
456 fn test_load_lint_config_from_toml() {
457 let dir = tempfile::tempdir().unwrap();
458 let toml_path = dir.path().join("foundry.toml");
459 fs::write(
460 &toml_path,
461 r#"
462[profile.default.lint]
463ignore = ["test/**/*"]
464lint_on_build = true
465"#,
466 )
467 .unwrap();
468
469 let config = load_lint_config_from_toml(&toml_path);
470 assert!(config.lint_on_build);
471 assert_eq!(config.ignore_patterns.len(), 1);
472 assert!(!config.should_lint(&dir.path().join("test/MyTest.sol")));
473 assert!(config.should_lint(&dir.path().join("src/Token.sol")));
474 }
475
476 #[test]
477 fn test_load_lint_config_lint_on_build_false() {
478 let dir = tempfile::tempdir().unwrap();
479 let toml_path = dir.path().join("foundry.toml");
480 fs::write(
481 &toml_path,
482 r#"
483[profile.default.lint]
484lint_on_build = false
485"#,
486 )
487 .unwrap();
488
489 let config = load_lint_config_from_toml(&toml_path);
490 assert!(!config.lint_on_build);
491 assert!(!config.should_lint(&dir.path().join("src/Token.sol")));
492 }
493
494 #[test]
495 fn test_load_lint_config_no_lint_section() {
496 let dir = tempfile::tempdir().unwrap();
497 let toml_path = dir.path().join("foundry.toml");
498 fs::write(
499 &toml_path,
500 r#"
501[profile.default]
502src = "src"
503"#,
504 )
505 .unwrap();
506
507 let config = load_lint_config_from_toml(&toml_path);
508 assert!(config.lint_on_build);
509 assert!(config.ignore_patterns.is_empty());
510 }
511
512 #[test]
513 fn test_find_foundry_toml() {
514 let dir = tempfile::tempdir().unwrap();
515 let toml_path = dir.path().join("foundry.toml");
516 fs::write(&toml_path, "[profile.default]").unwrap();
517
518 let nested = dir.path().join("src");
520 fs::create_dir_all(&nested).unwrap();
521
522 let found = find_foundry_toml(&nested);
523 assert_eq!(found, Some(toml_path));
524 }
525
526 #[test]
527 fn test_load_lint_config_walks_ancestors() {
528 let dir = tempfile::tempdir().unwrap();
529 let toml_path = dir.path().join("foundry.toml");
530 fs::write(
531 &toml_path,
532 r#"
533[profile.default.lint]
534ignore = ["test/**/*"]
535"#,
536 )
537 .unwrap();
538
539 let nested_file = dir.path().join("src/Token.sol");
540 fs::create_dir_all(dir.path().join("src")).unwrap();
541 fs::write(&nested_file, "// solidity").unwrap();
542
543 let config = load_lint_config(&nested_file);
544 assert_eq!(config.root, dir.path());
545 assert_eq!(config.ignore_patterns.len(), 1);
546 }
547
548 #[test]
549 fn test_find_git_root() {
550 let dir = tempfile::tempdir().unwrap();
551 fs::create_dir_all(dir.path().join(".git")).unwrap();
553 let nested = dir.path().join("sub/deep");
554 fs::create_dir_all(&nested).unwrap();
555
556 let root = find_git_root(&nested);
557 assert_eq!(root, Some(dir.path().to_path_buf()));
558 }
559
560 #[test]
561 fn test_find_foundry_toml_stops_at_git_boundary() {
562 let dir = tempfile::tempdir().unwrap();
570
571 fs::write(dir.path().join("foundry.toml"), "[profile.default]").unwrap();
573
574 let repo = dir.path().join("repo");
576 fs::create_dir_all(repo.join(".git")).unwrap();
577 fs::create_dir_all(repo.join("sub")).unwrap();
578
579 let found = find_foundry_toml(&repo.join("sub"));
580 assert_eq!(found, None);
582 }
583
584 #[test]
585 fn test_find_foundry_toml_within_git_boundary() {
586 let dir = tempfile::tempdir().unwrap();
594 let repo = dir.path().join("repo");
595 fs::create_dir_all(repo.join(".git")).unwrap();
596 fs::create_dir_all(repo.join("src")).unwrap();
597 let toml_path = repo.join("foundry.toml");
598 fs::write(&toml_path, "[profile.default]").unwrap();
599
600 let found = find_foundry_toml(&repo.join("src"));
601 assert_eq!(found, Some(toml_path));
602 }
603
604 #[test]
605 fn test_find_foundry_toml_no_git_repo_still_walks_up() {
606 let dir = tempfile::tempdir().unwrap();
609 let toml_path = dir.path().join("foundry.toml");
610 fs::write(&toml_path, "[profile.default]").unwrap();
611
612 let nested = dir.path().join("a/b/c");
613 fs::create_dir_all(&nested).unwrap();
614
615 let found = find_foundry_toml(&nested);
616 assert_eq!(found, Some(toml_path));
617 }
618
619 #[test]
622 fn test_load_foundry_config_compiler_settings() {
623 let dir = tempfile::tempdir().unwrap();
624 let toml_path = dir.path().join("foundry.toml");
625 fs::write(
626 &toml_path,
627 r#"
628[profile.default]
629src = "src"
630solc = '0.8.33'
631optimizer = true
632optimizer_runs = 9999999
633via_ir = true
634evm_version = 'osaka'
635ignored_error_codes = [2394, 6321, 3860, 5574, 2424, 8429, 4591]
636"#,
637 )
638 .unwrap();
639
640 let config = load_foundry_config_from_toml(&toml_path);
641 assert_eq!(config.solc_version, Some("0.8.33".to_string()));
642 assert!(config.optimizer);
643 assert_eq!(config.optimizer_runs, 9999999);
644 assert!(config.via_ir);
645 assert_eq!(config.evm_version, Some("osaka".to_string()));
646 assert_eq!(
647 config.ignored_error_codes,
648 vec![2394, 6321, 3860, 5574, 2424, 8429, 4591]
649 );
650 }
651
652 #[test]
653 fn test_load_foundry_config_defaults_when_absent() {
654 let dir = tempfile::tempdir().unwrap();
655 let toml_path = dir.path().join("foundry.toml");
656 fs::write(
657 &toml_path,
658 r#"
659[profile.default]
660src = "src"
661"#,
662 )
663 .unwrap();
664
665 let config = load_foundry_config_from_toml(&toml_path);
666 assert_eq!(config.solc_version, None);
667 assert!(!config.optimizer);
668 assert_eq!(config.optimizer_runs, 200);
669 assert!(!config.via_ir);
670 assert_eq!(config.evm_version, None);
671 assert!(config.ignored_error_codes.is_empty());
672 }
673
674 #[test]
675 fn test_load_foundry_config_partial_settings() {
676 let dir = tempfile::tempdir().unwrap();
677 let toml_path = dir.path().join("foundry.toml");
678 fs::write(
679 &toml_path,
680 r#"
681[profile.default]
682via_ir = true
683evm_version = "cancun"
684"#,
685 )
686 .unwrap();
687
688 let config = load_foundry_config_from_toml(&toml_path);
689 assert!(config.via_ir);
690 assert!(!config.optimizer); assert_eq!(config.optimizer_runs, 200); assert_eq!(config.evm_version, Some("cancun".to_string()));
693 assert!(config.ignored_error_codes.is_empty());
694 }
695}