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