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