1use serde::Deserialize;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, Deserialize)]
21#[serde(rename_all = "camelCase")]
22#[derive(Default)]
23pub struct Settings {
24 #[serde(default)]
25 pub inlay_hints: InlayHintsSettings,
26 #[serde(default)]
27 pub lint: LintSettings,
28 #[serde(default)]
29 pub file_operations: FileOperationsSettings,
30 #[serde(default)]
31 pub project_index: ProjectIndexSettings,
32 #[serde(default = "default_true")]
34 pub check_for_updates: bool,
35}
36
37#[derive(Debug, Clone, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct InlayHintsSettings {
41 #[serde(default = "default_true")]
43 pub parameters: bool,
44}
45
46impl Default for InlayHintsSettings {
47 fn default() -> Self {
48 Self { parameters: true }
49 }
50}
51
52#[derive(Debug, Clone, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct LintSettings {
56 #[serde(default = "default_true")]
58 pub enabled: bool,
59 #[serde(default)]
63 pub severity: Vec<String>,
64 #[serde(default)]
68 pub only: Vec<String>,
69 #[serde(default)]
72 pub exclude: Vec<String>,
73}
74
75impl Default for LintSettings {
76 fn default() -> Self {
77 Self {
78 enabled: true,
79 severity: Vec::new(),
80 only: Vec::new(),
81 exclude: Vec::new(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct FileOperationsSettings {
90 #[serde(default = "default_true")]
92 pub template_on_create: bool,
93 #[serde(default = "default_true")]
95 pub update_imports_on_rename: bool,
96 #[serde(default = "default_true")]
98 pub update_imports_on_delete: bool,
99}
100
101impl Default for FileOperationsSettings {
102 fn default() -> Self {
103 Self {
104 template_on_create: true,
105 update_imports_on_rename: true,
106 update_imports_on_delete: true,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct ProjectIndexSettings {
115 #[serde(default)]
118 pub full_project_scan: bool,
119 #[serde(default)]
123 pub cache_mode: ProjectIndexCacheMode,
124 #[serde(default)]
128 pub incremental_edit_reindex: bool,
129}
130
131#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
132#[serde(rename_all = "lowercase")]
133pub enum ProjectIndexCacheMode {
134 Auto,
135 #[default]
136 V2,
137}
138
139impl Default for ProjectIndexSettings {
140 fn default() -> Self {
141 Self {
142 full_project_scan: true,
143 cache_mode: ProjectIndexCacheMode::V2,
144 incremental_edit_reindex: false,
145 }
146 }
147}
148
149fn default_true() -> bool {
150 true
151}
152
153fn active_profile_name() -> String {
154 std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string())
155}
156
157fn find_profile_table<'a>(table: &'a toml::Table, profile_name: &str) -> Option<&'a toml::Table> {
158 table
159 .get("profile")
160 .and_then(|p| p.as_table())
161 .and_then(|profiles| profiles.get(profile_name))
162 .and_then(|p| p.as_table())
163}
164
165fn get_profile_value<'a>(
166 active_profile: Option<&'a toml::Table>,
167 default_profile: Option<&'a toml::Table>,
168 key: &str,
169) -> Option<&'a toml::Value> {
170 active_profile
171 .and_then(|p| p.get(key))
172 .or_else(|| default_profile.and_then(|p| p.get(key)))
173}
174
175fn get_lint_table<'a>(
176 active_profile: Option<&'a toml::Table>,
177 default_profile: Option<&'a toml::Table>,
178) -> Option<&'a toml::Table> {
179 active_profile
180 .and_then(|p| p.get("lint"))
181 .and_then(|l| l.as_table())
182 .or_else(|| {
183 default_profile
184 .and_then(|p| p.get("lint"))
185 .and_then(|l| l.as_table())
186 })
187}
188
189pub fn parse_settings(value: &serde_json::Value) -> Settings {
197 if let Some(inner) = value.get("solidity-language-server")
199 && let Ok(s) = serde_json::from_value::<Settings>(inner.clone())
200 {
201 return s;
202 }
203 serde_json::from_value::<Settings>(value.clone()).unwrap_or_default()
205}
206
207#[derive(Debug, Clone)]
212pub struct FoundryConfig {
213 pub root: PathBuf,
215 pub solc_version: Option<String>,
218 pub remappings: Vec<String>,
221 pub via_ir: bool,
224 pub optimizer: bool,
226 pub optimizer_runs: u64,
229 pub evm_version: Option<String>,
233 pub ignored_error_codes: Vec<u64>,
235 pub sources_dir: String,
237 pub test_dir: String,
240 pub script_dir: String,
243 pub libs: Vec<String>,
246}
247
248impl Default for FoundryConfig {
249 fn default() -> Self {
250 Self {
251 root: PathBuf::new(),
252 solc_version: None,
253 remappings: Vec::new(),
254 via_ir: false,
255 optimizer: false,
256 optimizer_runs: 200,
257 evm_version: None,
258 ignored_error_codes: Vec::new(),
259 sources_dir: "src".to_string(),
260 test_dir: "test".to_string(),
261 script_dir: "script".to_string(),
262 libs: vec!["lib".to_string()],
263 }
264 }
265}
266
267pub fn load_foundry_config(file_path: &Path) -> FoundryConfig {
274 let toml_path = match find_foundry_toml(file_path) {
275 Some(p) => p,
276 None => {
277 let start = if file_path.is_file() {
278 file_path.parent().unwrap_or(file_path)
279 } else {
280 file_path
281 };
282 let root = find_git_root(start).unwrap_or_else(|| start.to_path_buf());
283 return FoundryConfig {
284 root,
285 ..Default::default()
286 };
287 }
288 };
289 load_foundry_config_from_toml(&toml_path)
290}
291
292pub fn load_foundry_config_from_toml(toml_path: &Path) -> FoundryConfig {
294 load_foundry_config_from_toml_with_profile_name(toml_path, &active_profile_name())
295}
296
297fn load_foundry_config_from_toml_with_profile_name(
298 toml_path: &Path,
299 profile_name: &str,
300) -> FoundryConfig {
301 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
302
303 let content = match std::fs::read_to_string(toml_path) {
304 Ok(c) => c,
305 Err(_) => {
306 return FoundryConfig {
307 root,
308 ..Default::default()
309 };
310 }
311 };
312
313 let table: toml::Table = match content.parse() {
314 Ok(t) => t,
315 Err(_) => {
316 return FoundryConfig {
317 root,
318 ..Default::default()
319 };
320 }
321 };
322
323 let active_profile = find_profile_table(&table, profile_name);
324 let default_profile = find_profile_table(&table, "default");
325 if active_profile.is_none() && default_profile.is_none() {
326 return FoundryConfig {
327 root,
328 ..Default::default()
329 };
330 }
331
332 let solc_version = get_profile_value(active_profile, default_profile, "solc")
334 .or_else(|| get_profile_value(active_profile, default_profile, "solc_version"))
335 .and_then(|v| v.as_str())
336 .map(|s| s.to_string());
337
338 let remappings = get_profile_value(active_profile, default_profile, "remappings")
340 .and_then(|v| v.as_array())
341 .map(|arr| {
342 arr.iter()
343 .filter_map(|v| v.as_str())
344 .map(|s| s.to_string())
345 .collect()
346 })
347 .unwrap_or_default();
348
349 let via_ir = get_profile_value(active_profile, default_profile, "via_ir")
351 .and_then(|v| v.as_bool())
352 .unwrap_or(false);
353
354 let optimizer = get_profile_value(active_profile, default_profile, "optimizer")
356 .and_then(|v| v.as_bool())
357 .unwrap_or(false);
358
359 let optimizer_runs = get_profile_value(active_profile, default_profile, "optimizer_runs")
361 .and_then(|v| v.as_integer())
362 .map(|v| v as u64)
363 .unwrap_or(200);
364
365 let evm_version = get_profile_value(active_profile, default_profile, "evm_version")
367 .and_then(|v| v.as_str())
368 .map(|s| s.to_string());
369
370 let sources_dir = get_profile_value(active_profile, default_profile, "src")
372 .and_then(|v| v.as_str())
373 .map(|s| s.to_string())
374 .unwrap_or_else(|| "src".to_string());
375
376 let test_dir = get_profile_value(active_profile, default_profile, "test")
378 .and_then(|v| v.as_str())
379 .map(|s| s.to_string())
380 .unwrap_or_else(|| "test".to_string());
381
382 let script_dir = get_profile_value(active_profile, default_profile, "script")
384 .and_then(|v| v.as_str())
385 .map(|s| s.to_string())
386 .unwrap_or_else(|| "script".to_string());
387
388 let libs = get_profile_value(active_profile, default_profile, "libs")
390 .and_then(|v| v.as_array())
391 .map(|arr| {
392 arr.iter()
393 .filter_map(|v| v.as_str())
394 .map(|s| s.to_string())
395 .collect()
396 })
397 .unwrap_or_else(|| vec!["lib".to_string()]);
398
399 let ignored_error_codes =
401 get_profile_value(active_profile, default_profile, "ignored_error_codes")
402 .and_then(|v| v.as_array())
403 .map(|arr| {
404 arr.iter()
405 .filter_map(|v| v.as_integer())
406 .map(|v| v as u64)
407 .collect()
408 })
409 .unwrap_or_default();
410
411 FoundryConfig {
412 root,
413 solc_version,
414 remappings,
415 via_ir,
416 optimizer,
417 optimizer_runs,
418 evm_version,
419 ignored_error_codes,
420 sources_dir,
421 test_dir,
422 script_dir,
423 libs,
424 }
425}
426
427#[derive(Debug, Clone)]
429pub struct LintConfig {
430 pub root: PathBuf,
432 pub lint_on_build: bool,
434 pub ignore_patterns: Vec<glob::Pattern>,
436}
437
438impl Default for LintConfig {
439 fn default() -> Self {
440 Self {
441 root: PathBuf::new(),
442 lint_on_build: true,
443 ignore_patterns: Vec::new(),
444 }
445 }
446}
447
448impl LintConfig {
449 pub fn should_lint(&self, file_path: &Path) -> bool {
455 if !self.lint_on_build {
456 return false;
457 }
458
459 if self.ignore_patterns.is_empty() {
460 return true;
461 }
462
463 let relative = file_path.strip_prefix(&self.root).unwrap_or(file_path);
466
467 let rel_str = relative.to_string_lossy();
468
469 for pattern in &self.ignore_patterns {
470 if pattern.matches(&rel_str) {
471 return false;
472 }
473 }
474
475 true
476 }
477}
478
479fn find_git_root(start: &Path) -> Option<PathBuf> {
484 let start = if start.is_file() {
485 start.parent()?
486 } else {
487 start
488 };
489 start
490 .ancestors()
491 .find(|p| p.join(".git").exists())
492 .map(Path::to_path_buf)
493}
494
495pub fn find_foundry_toml(start: &Path) -> Option<PathBuf> {
500 let start_dir = if start.is_file() {
501 start.parent()?
502 } else {
503 start
504 };
505
506 let boundary = find_git_root(start_dir);
507
508 start_dir
509 .ancestors()
510 .take_while(|p| {
512 if let Some(boundary) = &boundary {
513 p.starts_with(boundary)
514 } else {
515 true
516 }
517 })
518 .find(|p| p.join("foundry.toml").is_file())
519 .map(|p| p.join("foundry.toml"))
520}
521
522pub fn load_lint_config(file_path: &Path) -> LintConfig {
526 let toml_path = match find_foundry_toml(file_path) {
527 Some(p) => p,
528 None => return LintConfig::default(),
529 };
530 load_lint_config_from_toml(&toml_path)
531}
532
533pub fn load_lint_config_from_toml(toml_path: &Path) -> LintConfig {
536 load_lint_config_from_toml_with_profile_name(toml_path, &active_profile_name())
537}
538
539fn load_lint_config_from_toml_with_profile_name(
540 toml_path: &Path,
541 profile_name: &str,
542) -> LintConfig {
543 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
544
545 let content = match std::fs::read_to_string(toml_path) {
546 Ok(c) => c,
547 Err(_) => {
548 return LintConfig {
549 root,
550 ..Default::default()
551 };
552 }
553 };
554
555 let table: toml::Table = match content.parse() {
556 Ok(t) => t,
557 Err(_) => {
558 return LintConfig {
559 root,
560 ..Default::default()
561 };
562 }
563 };
564
565 let active_profile = find_profile_table(&table, profile_name);
566 let default_profile = find_profile_table(&table, "default");
567 let lint_table = get_lint_table(active_profile, default_profile);
568
569 let lint_table = match lint_table {
570 Some(t) => t,
571 None => {
572 return LintConfig {
573 root,
574 ..Default::default()
575 };
576 }
577 };
578
579 let lint_on_build = lint_table
580 .get("lint_on_build")
581 .and_then(|v| v.as_bool())
582 .unwrap_or(true);
583
584 let ignore_patterns = lint_table
585 .get("ignore")
586 .and_then(|v| v.as_array())
587 .map(|arr| {
588 arr.iter()
589 .filter_map(|v| v.as_str())
590 .filter_map(|s| glob::Pattern::new(s).ok())
591 .collect()
592 })
593 .unwrap_or_default();
594
595 LintConfig {
596 root,
597 lint_on_build,
598 ignore_patterns,
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use std::fs;
606
607 #[test]
608 fn test_default_config_lints_everything() {
609 let config = LintConfig::default();
610 assert!(config.should_lint(Path::new("test/MyTest.sol")));
611 assert!(config.should_lint(Path::new("src/Token.sol")));
612 }
613
614 #[test]
615 fn test_lint_on_build_false_skips_all() {
616 let config = LintConfig {
617 lint_on_build: false,
618 ..Default::default()
619 };
620 assert!(!config.should_lint(Path::new("src/Token.sol")));
621 }
622
623 #[test]
624 fn test_ignore_pattern_matches() {
625 let config = LintConfig {
626 root: PathBuf::from("/project"),
627 lint_on_build: true,
628 ignore_patterns: vec![glob::Pattern::new("test/**/*").unwrap()],
629 };
630 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
631 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
632 }
633
634 #[test]
635 fn test_multiple_ignore_patterns() {
636 let config = LintConfig {
637 root: PathBuf::from("/project"),
638 lint_on_build: true,
639 ignore_patterns: vec![
640 glob::Pattern::new("test/**/*").unwrap(),
641 glob::Pattern::new("script/**/*").unwrap(),
642 ],
643 };
644 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
645 assert!(!config.should_lint(Path::new("/project/script/Deploy.sol")));
646 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
647 }
648
649 #[test]
650 fn test_load_lint_config_from_toml() {
651 let dir = tempfile::tempdir().unwrap();
652 let toml_path = dir.path().join("foundry.toml");
653 fs::write(
654 &toml_path,
655 r#"
656[profile.default.lint]
657ignore = ["test/**/*"]
658lint_on_build = true
659"#,
660 )
661 .unwrap();
662
663 let config = load_lint_config_from_toml(&toml_path);
664 assert!(config.lint_on_build);
665 assert_eq!(config.ignore_patterns.len(), 1);
666 assert!(!config.should_lint(&dir.path().join("test/MyTest.sol")));
667 assert!(config.should_lint(&dir.path().join("src/Token.sol")));
668 }
669
670 #[test]
671 fn test_load_lint_config_lint_on_build_false() {
672 let dir = tempfile::tempdir().unwrap();
673 let toml_path = dir.path().join("foundry.toml");
674 fs::write(
675 &toml_path,
676 r#"
677[profile.default.lint]
678lint_on_build = false
679"#,
680 )
681 .unwrap();
682
683 let config = load_lint_config_from_toml(&toml_path);
684 assert!(!config.lint_on_build);
685 assert!(!config.should_lint(&dir.path().join("src/Token.sol")));
686 }
687
688 #[test]
689 fn test_load_lint_config_no_lint_section() {
690 let dir = tempfile::tempdir().unwrap();
691 let toml_path = dir.path().join("foundry.toml");
692 fs::write(
693 &toml_path,
694 r#"
695[profile.default]
696src = "src"
697"#,
698 )
699 .unwrap();
700
701 let config = load_lint_config_from_toml(&toml_path);
702 assert!(config.lint_on_build);
703 assert!(config.ignore_patterns.is_empty());
704 }
705
706 #[test]
707 fn test_load_lint_config_falls_back_to_default_lint_section() {
708 let dir = tempfile::tempdir().unwrap();
709 let toml_path = dir.path().join("foundry.toml");
710 fs::write(
711 &toml_path,
712 r#"
713[profile.default.lint]
714ignore = ["test/**/*"]
715lint_on_build = false
716
717[profile.local]
718src = "src"
719"#,
720 )
721 .unwrap();
722
723 let config = load_lint_config_from_toml_with_profile_name(&toml_path, "local");
724 assert!(!config.lint_on_build);
725 assert_eq!(config.ignore_patterns.len(), 1);
726 assert!(!config.should_lint(&dir.path().join("test/MyTest.sol")));
727 }
728
729 #[test]
730 fn test_find_foundry_toml() {
731 let dir = tempfile::tempdir().unwrap();
732 let toml_path = dir.path().join("foundry.toml");
733 fs::write(&toml_path, "[profile.default]").unwrap();
734
735 let nested = dir.path().join("src");
737 fs::create_dir_all(&nested).unwrap();
738
739 let found = find_foundry_toml(&nested);
740 assert_eq!(found, Some(toml_path));
741 }
742
743 #[test]
744 fn test_load_lint_config_walks_ancestors() {
745 let dir = tempfile::tempdir().unwrap();
746 let toml_path = dir.path().join("foundry.toml");
747 fs::write(
748 &toml_path,
749 r#"
750[profile.default.lint]
751ignore = ["test/**/*"]
752"#,
753 )
754 .unwrap();
755
756 let nested_file = dir.path().join("src/Token.sol");
757 fs::create_dir_all(dir.path().join("src")).unwrap();
758 fs::write(&nested_file, "// solidity").unwrap();
759
760 let config = load_lint_config(&nested_file);
761 assert_eq!(config.root, dir.path());
762 assert_eq!(config.ignore_patterns.len(), 1);
763 }
764
765 #[test]
766 fn test_find_git_root() {
767 let dir = tempfile::tempdir().unwrap();
768 fs::create_dir_all(dir.path().join(".git")).unwrap();
770 let nested = dir.path().join("sub/deep");
771 fs::create_dir_all(&nested).unwrap();
772
773 let root = find_git_root(&nested);
774 assert_eq!(root, Some(dir.path().to_path_buf()));
775 }
776
777 #[test]
778 fn test_find_foundry_toml_stops_at_git_boundary() {
779 let dir = tempfile::tempdir().unwrap();
787
788 fs::write(dir.path().join("foundry.toml"), "[profile.default]").unwrap();
790
791 let repo = dir.path().join("repo");
793 fs::create_dir_all(repo.join(".git")).unwrap();
794 fs::create_dir_all(repo.join("sub")).unwrap();
795
796 let found = find_foundry_toml(&repo.join("sub"));
797 assert_eq!(found, None);
799 }
800
801 #[test]
802 fn test_find_foundry_toml_within_git_boundary() {
803 let dir = tempfile::tempdir().unwrap();
811 let repo = dir.path().join("repo");
812 fs::create_dir_all(repo.join(".git")).unwrap();
813 fs::create_dir_all(repo.join("src")).unwrap();
814 let toml_path = repo.join("foundry.toml");
815 fs::write(&toml_path, "[profile.default]").unwrap();
816
817 let found = find_foundry_toml(&repo.join("src"));
818 assert_eq!(found, Some(toml_path));
819 }
820
821 #[test]
822 fn test_find_foundry_toml_no_git_repo_still_walks_up() {
823 let dir = tempfile::tempdir().unwrap();
826 let toml_path = dir.path().join("foundry.toml");
827 fs::write(&toml_path, "[profile.default]").unwrap();
828
829 let nested = dir.path().join("a/b/c");
830 fs::create_dir_all(&nested).unwrap();
831
832 let found = find_foundry_toml(&nested);
833 assert_eq!(found, Some(toml_path));
834 }
835
836 #[test]
839 fn test_load_foundry_config_compiler_settings() {
840 let dir = tempfile::tempdir().unwrap();
841 let toml_path = dir.path().join("foundry.toml");
842 fs::write(
843 &toml_path,
844 r#"
845[profile.default]
846src = "src"
847solc = '0.8.33'
848optimizer = true
849optimizer_runs = 9999999
850via_ir = true
851evm_version = 'osaka'
852ignored_error_codes = [2394, 6321, 3860, 5574, 2424, 8429, 4591]
853"#,
854 )
855 .unwrap();
856
857 let config = load_foundry_config_from_toml(&toml_path);
858 assert_eq!(config.solc_version, Some("0.8.33".to_string()));
859 assert!(config.optimizer);
860 assert_eq!(config.optimizer_runs, 9999999);
861 assert!(config.via_ir);
862 assert_eq!(config.evm_version, Some("osaka".to_string()));
863 assert_eq!(
864 config.ignored_error_codes,
865 vec![2394, 6321, 3860, 5574, 2424, 8429, 4591]
866 );
867 }
868
869 #[test]
870 fn test_load_foundry_config_defaults_when_absent() {
871 let dir = tempfile::tempdir().unwrap();
872 let toml_path = dir.path().join("foundry.toml");
873 fs::write(
874 &toml_path,
875 r#"
876[profile.default]
877src = "src"
878"#,
879 )
880 .unwrap();
881
882 let config = load_foundry_config_from_toml(&toml_path);
883 assert_eq!(config.solc_version, None);
884 assert!(!config.optimizer);
885 assert_eq!(config.optimizer_runs, 200);
886 assert!(!config.via_ir);
887 assert_eq!(config.evm_version, None);
888 assert!(config.ignored_error_codes.is_empty());
889 assert_eq!(config.libs, vec!["lib".to_string()]);
890 }
891
892 #[test]
893 fn test_load_foundry_config_partial_settings() {
894 let dir = tempfile::tempdir().unwrap();
895 let toml_path = dir.path().join("foundry.toml");
896 fs::write(
897 &toml_path,
898 r#"
899[profile.default]
900via_ir = true
901evm_version = "cancun"
902"#,
903 )
904 .unwrap();
905
906 let config = load_foundry_config_from_toml(&toml_path);
907 assert!(config.via_ir);
908 assert!(!config.optimizer); assert_eq!(config.optimizer_runs, 200); assert_eq!(config.evm_version, Some("cancun".to_string()));
911 assert!(config.ignored_error_codes.is_empty());
912 }
913
914 #[test]
915 fn test_load_foundry_config_libs() {
916 let dir = tempfile::tempdir().unwrap();
917 let toml_path = dir.path().join("foundry.toml");
918 fs::write(
919 &toml_path,
920 r#"
921[profile.default]
922libs = ["lib", "node_modules", "dependencies"]
923"#,
924 )
925 .unwrap();
926
927 let config = load_foundry_config_from_toml(&toml_path);
928 assert_eq!(
929 config.libs,
930 vec![
931 "lib".to_string(),
932 "node_modules".to_string(),
933 "dependencies".to_string()
934 ]
935 );
936 }
937
938 #[test]
939 fn test_load_foundry_config_libs_defaults_when_absent() {
940 let dir = tempfile::tempdir().unwrap();
941 let toml_path = dir.path().join("foundry.toml");
942 fs::write(
943 &toml_path,
944 r#"
945[profile.default]
946src = "src"
947"#,
948 )
949 .unwrap();
950
951 let config = load_foundry_config_from_toml(&toml_path);
952 assert_eq!(config.libs, vec!["lib".to_string()]);
953 }
954
955 #[test]
956 fn test_load_foundry_config_falls_back_to_default_profile_values() {
957 let dir = tempfile::tempdir().unwrap();
958 let toml_path = dir.path().join("foundry.toml");
959 fs::write(
960 &toml_path,
961 r#"
962[profile.default]
963solc = "0.8.33"
964optimizer_runs = 1234
965libs = ["lib", "node_modules"]
966
967[profile.local]
968src = "contracts"
969"#,
970 )
971 .unwrap();
972
973 let config = load_foundry_config_from_toml_with_profile_name(&toml_path, "local");
974 assert_eq!(config.solc_version, Some("0.8.33".to_string()));
975 assert_eq!(config.optimizer_runs, 1234);
976 assert_eq!(
977 config.libs,
978 vec!["lib".to_string(), "node_modules".to_string()]
979 );
980 assert_eq!(config.sources_dir, "contracts".to_string());
981 }
982
983 #[test]
986 fn test_parse_settings_defaults() {
987 let value = serde_json::json!({});
988 let s = parse_settings(&value);
989 assert!(s.inlay_hints.parameters);
990 assert!(s.lint.enabled);
991 assert!(s.file_operations.template_on_create);
992 assert!(s.file_operations.update_imports_on_rename);
993 assert!(s.file_operations.update_imports_on_delete);
994 assert!(s.project_index.full_project_scan);
995 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
996 assert!(!s.project_index.incremental_edit_reindex);
997 assert!(s.lint.severity.is_empty());
998 assert!(s.lint.only.is_empty());
999 assert!(s.lint.exclude.is_empty());
1000 }
1001
1002 #[test]
1003 fn test_parse_settings_wrapped() {
1004 let value = serde_json::json!({
1005 "solidity-language-server": {
1006 "inlayHints": { "parameters": false },
1007 "lint": {
1008 "enabled": true,
1009 "severity": ["high", "med"],
1010 "only": ["incorrect-shift"],
1011 "exclude": ["pascal-case-struct", "mixed-case-variable"]
1012 },
1013 "fileOperations": {
1014 "templateOnCreate": false,
1015 "updateImportsOnRename": false,
1016 "updateImportsOnDelete": false
1017 },
1018 "projectIndex": {
1019 "fullProjectScan": true,
1020 "cacheMode": "v2",
1021 "incrementalEditReindex": true
1022 },
1023 }
1024 });
1025 let s = parse_settings(&value);
1026 assert!(!s.inlay_hints.parameters);
1027 assert!(s.lint.enabled);
1028 assert!(!s.file_operations.template_on_create);
1029 assert!(!s.file_operations.update_imports_on_rename);
1030 assert!(!s.file_operations.update_imports_on_delete);
1031 assert!(s.project_index.full_project_scan);
1032 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1033 assert!(s.project_index.incremental_edit_reindex);
1034 assert_eq!(s.lint.severity, vec!["high", "med"]);
1035 assert_eq!(s.lint.only, vec!["incorrect-shift"]);
1036 assert_eq!(
1037 s.lint.exclude,
1038 vec!["pascal-case-struct", "mixed-case-variable"]
1039 );
1040 }
1041
1042 #[test]
1043 fn test_parse_settings_direct() {
1044 let value = serde_json::json!({
1045 "inlayHints": { "parameters": false },
1046 "lint": { "enabled": false },
1047 "fileOperations": {
1048 "templateOnCreate": false,
1049 "updateImportsOnRename": false,
1050 "updateImportsOnDelete": false
1051 },
1052 "projectIndex": {
1053 "fullProjectScan": true,
1054 "cacheMode": "v2",
1055 "incrementalEditReindex": true
1056 }
1057 });
1058 let s = parse_settings(&value);
1059 assert!(!s.inlay_hints.parameters);
1060 assert!(!s.lint.enabled);
1061 assert!(!s.file_operations.template_on_create);
1062 assert!(!s.file_operations.update_imports_on_rename);
1063 assert!(!s.file_operations.update_imports_on_delete);
1064 assert!(s.project_index.full_project_scan);
1065 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1066 assert!(s.project_index.incremental_edit_reindex);
1067 }
1068
1069 #[test]
1070 fn test_parse_settings_partial() {
1071 let value = serde_json::json!({
1072 "solidity-language-server": {
1073 "lint": { "exclude": ["unused-import"] }
1074 }
1075 });
1076 let s = parse_settings(&value);
1077 assert!(s.inlay_hints.parameters);
1079 assert!(s.lint.enabled);
1081 assert!(s.file_operations.template_on_create);
1082 assert!(s.file_operations.update_imports_on_rename);
1083 assert!(s.file_operations.update_imports_on_delete);
1084 assert!(s.project_index.full_project_scan);
1085 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1086 assert!(!s.project_index.incremental_edit_reindex);
1087 assert!(s.lint.severity.is_empty());
1088 assert!(s.lint.only.is_empty());
1089 assert_eq!(s.lint.exclude, vec!["unused-import"]);
1090 }
1091
1092 #[test]
1093 fn test_parse_settings_empty_wrapped() {
1094 let value = serde_json::json!({
1095 "solidity-language-server": {}
1096 });
1097 let s = parse_settings(&value);
1098 assert!(s.inlay_hints.parameters);
1099 assert!(s.lint.enabled);
1100 assert!(s.file_operations.template_on_create);
1101 assert!(s.file_operations.update_imports_on_rename);
1102 assert!(s.file_operations.update_imports_on_delete);
1103 assert!(s.project_index.full_project_scan);
1104 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1105 assert!(!s.project_index.incremental_edit_reindex);
1106 assert!(s.lint.severity.is_empty());
1107 assert!(s.lint.only.is_empty());
1108 assert!(s.lint.exclude.is_empty());
1109 }
1110
1111 #[test]
1112 fn test_parse_settings_project_index_cache_mode_defaults_on_invalid() {
1113 let value = serde_json::json!({
1114 "solidity-language-server": {
1115 "projectIndex": {
1116 "cacheMode": "bad-mode"
1117 }
1118 }
1119 });
1120 let s = parse_settings(&value);
1121 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1122 assert!(!s.project_index.incremental_edit_reindex);
1123 }
1124
1125 #[test]
1126 fn test_parse_settings_severity_only() {
1127 let value = serde_json::json!({
1128 "solidity-language-server": {
1129 "lint": {
1130 "severity": ["high", "gas"],
1131 "only": ["incorrect-shift", "asm-keccak256"]
1132 }
1133 }
1134 });
1135 let s = parse_settings(&value);
1136 assert_eq!(s.lint.severity, vec!["high", "gas"]);
1137 assert_eq!(s.lint.only, vec!["incorrect-shift", "asm-keccak256"]);
1138 assert!(s.lint.exclude.is_empty());
1139 }
1140}