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 = get_profile_value(active_profile, default_profile, "ignored_error_codes")
385 .and_then(|v| v.as_array())
386 .map(|arr| {
387 arr.iter()
388 .filter_map(|v| v.as_integer())
389 .map(|v| v as u64)
390 .collect()
391 })
392 .unwrap_or_default();
393
394 FoundryConfig {
395 root,
396 solc_version,
397 remappings,
398 via_ir,
399 optimizer,
400 optimizer_runs,
401 evm_version,
402 ignored_error_codes,
403 sources_dir,
404 libs,
405 }
406}
407
408#[derive(Debug, Clone)]
410pub struct LintConfig {
411 pub root: PathBuf,
413 pub lint_on_build: bool,
415 pub ignore_patterns: Vec<glob::Pattern>,
417}
418
419impl Default for LintConfig {
420 fn default() -> Self {
421 Self {
422 root: PathBuf::new(),
423 lint_on_build: true,
424 ignore_patterns: Vec::new(),
425 }
426 }
427}
428
429impl LintConfig {
430 pub fn should_lint(&self, file_path: &Path) -> bool {
436 if !self.lint_on_build {
437 return false;
438 }
439
440 if self.ignore_patterns.is_empty() {
441 return true;
442 }
443
444 let relative = file_path.strip_prefix(&self.root).unwrap_or(file_path);
447
448 let rel_str = relative.to_string_lossy();
449
450 for pattern in &self.ignore_patterns {
451 if pattern.matches(&rel_str) {
452 return false;
453 }
454 }
455
456 true
457 }
458}
459
460fn find_git_root(start: &Path) -> Option<PathBuf> {
465 let start = if start.is_file() {
466 start.parent()?
467 } else {
468 start
469 };
470 start
471 .ancestors()
472 .find(|p| p.join(".git").exists())
473 .map(Path::to_path_buf)
474}
475
476pub fn find_foundry_toml(start: &Path) -> Option<PathBuf> {
481 let start_dir = if start.is_file() {
482 start.parent()?
483 } else {
484 start
485 };
486
487 let boundary = find_git_root(start_dir);
488
489 start_dir
490 .ancestors()
491 .take_while(|p| {
493 if let Some(boundary) = &boundary {
494 p.starts_with(boundary)
495 } else {
496 true
497 }
498 })
499 .find(|p| p.join("foundry.toml").is_file())
500 .map(|p| p.join("foundry.toml"))
501}
502
503pub fn load_lint_config(file_path: &Path) -> LintConfig {
507 let toml_path = match find_foundry_toml(file_path) {
508 Some(p) => p,
509 None => return LintConfig::default(),
510 };
511 load_lint_config_from_toml(&toml_path)
512}
513
514pub fn load_lint_config_from_toml(toml_path: &Path) -> LintConfig {
517 load_lint_config_from_toml_with_profile_name(toml_path, &active_profile_name())
518}
519
520fn load_lint_config_from_toml_with_profile_name(toml_path: &Path, profile_name: &str) -> LintConfig {
521 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
522
523 let content = match std::fs::read_to_string(toml_path) {
524 Ok(c) => c,
525 Err(_) => {
526 return LintConfig {
527 root,
528 ..Default::default()
529 };
530 }
531 };
532
533 let table: toml::Table = match content.parse() {
534 Ok(t) => t,
535 Err(_) => {
536 return LintConfig {
537 root,
538 ..Default::default()
539 };
540 }
541 };
542
543 let active_profile = find_profile_table(&table, profile_name);
544 let default_profile = find_profile_table(&table, "default");
545 let lint_table = get_lint_table(active_profile, default_profile);
546
547 let lint_table = match lint_table {
548 Some(t) => t,
549 None => {
550 return LintConfig {
551 root,
552 ..Default::default()
553 };
554 }
555 };
556
557 let lint_on_build = lint_table
558 .get("lint_on_build")
559 .and_then(|v| v.as_bool())
560 .unwrap_or(true);
561
562 let ignore_patterns = lint_table
563 .get("ignore")
564 .and_then(|v| v.as_array())
565 .map(|arr| {
566 arr.iter()
567 .filter_map(|v| v.as_str())
568 .filter_map(|s| glob::Pattern::new(s).ok())
569 .collect()
570 })
571 .unwrap_or_default();
572
573 LintConfig {
574 root,
575 lint_on_build,
576 ignore_patterns,
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use std::fs;
584
585 #[test]
586 fn test_default_config_lints_everything() {
587 let config = LintConfig::default();
588 assert!(config.should_lint(Path::new("test/MyTest.sol")));
589 assert!(config.should_lint(Path::new("src/Token.sol")));
590 }
591
592 #[test]
593 fn test_lint_on_build_false_skips_all() {
594 let config = LintConfig {
595 lint_on_build: false,
596 ..Default::default()
597 };
598 assert!(!config.should_lint(Path::new("src/Token.sol")));
599 }
600
601 #[test]
602 fn test_ignore_pattern_matches() {
603 let config = LintConfig {
604 root: PathBuf::from("/project"),
605 lint_on_build: true,
606 ignore_patterns: vec![glob::Pattern::new("test/**/*").unwrap()],
607 };
608 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
609 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
610 }
611
612 #[test]
613 fn test_multiple_ignore_patterns() {
614 let config = LintConfig {
615 root: PathBuf::from("/project"),
616 lint_on_build: true,
617 ignore_patterns: vec![
618 glob::Pattern::new("test/**/*").unwrap(),
619 glob::Pattern::new("script/**/*").unwrap(),
620 ],
621 };
622 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
623 assert!(!config.should_lint(Path::new("/project/script/Deploy.sol")));
624 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
625 }
626
627 #[test]
628 fn test_load_lint_config_from_toml() {
629 let dir = tempfile::tempdir().unwrap();
630 let toml_path = dir.path().join("foundry.toml");
631 fs::write(
632 &toml_path,
633 r#"
634[profile.default.lint]
635ignore = ["test/**/*"]
636lint_on_build = true
637"#,
638 )
639 .unwrap();
640
641 let config = load_lint_config_from_toml(&toml_path);
642 assert!(config.lint_on_build);
643 assert_eq!(config.ignore_patterns.len(), 1);
644 assert!(!config.should_lint(&dir.path().join("test/MyTest.sol")));
645 assert!(config.should_lint(&dir.path().join("src/Token.sol")));
646 }
647
648 #[test]
649 fn test_load_lint_config_lint_on_build_false() {
650 let dir = tempfile::tempdir().unwrap();
651 let toml_path = dir.path().join("foundry.toml");
652 fs::write(
653 &toml_path,
654 r#"
655[profile.default.lint]
656lint_on_build = false
657"#,
658 )
659 .unwrap();
660
661 let config = load_lint_config_from_toml(&toml_path);
662 assert!(!config.lint_on_build);
663 assert!(!config.should_lint(&dir.path().join("src/Token.sol")));
664 }
665
666 #[test]
667 fn test_load_lint_config_no_lint_section() {
668 let dir = tempfile::tempdir().unwrap();
669 let toml_path = dir.path().join("foundry.toml");
670 fs::write(
671 &toml_path,
672 r#"
673[profile.default]
674src = "src"
675"#,
676 )
677 .unwrap();
678
679 let config = load_lint_config_from_toml(&toml_path);
680 assert!(config.lint_on_build);
681 assert!(config.ignore_patterns.is_empty());
682 }
683
684 #[test]
685 fn test_load_lint_config_falls_back_to_default_lint_section() {
686 let dir = tempfile::tempdir().unwrap();
687 let toml_path = dir.path().join("foundry.toml");
688 fs::write(
689 &toml_path,
690 r#"
691[profile.default.lint]
692ignore = ["test/**/*"]
693lint_on_build = false
694
695[profile.local]
696src = "src"
697"#,
698 )
699 .unwrap();
700
701 let config = load_lint_config_from_toml_with_profile_name(&toml_path, "local");
702 assert!(!config.lint_on_build);
703 assert_eq!(config.ignore_patterns.len(), 1);
704 assert!(!config.should_lint(&dir.path().join("test/MyTest.sol")));
705 }
706
707 #[test]
708 fn test_find_foundry_toml() {
709 let dir = tempfile::tempdir().unwrap();
710 let toml_path = dir.path().join("foundry.toml");
711 fs::write(&toml_path, "[profile.default]").unwrap();
712
713 let nested = dir.path().join("src");
715 fs::create_dir_all(&nested).unwrap();
716
717 let found = find_foundry_toml(&nested);
718 assert_eq!(found, Some(toml_path));
719 }
720
721 #[test]
722 fn test_load_lint_config_walks_ancestors() {
723 let dir = tempfile::tempdir().unwrap();
724 let toml_path = dir.path().join("foundry.toml");
725 fs::write(
726 &toml_path,
727 r#"
728[profile.default.lint]
729ignore = ["test/**/*"]
730"#,
731 )
732 .unwrap();
733
734 let nested_file = dir.path().join("src/Token.sol");
735 fs::create_dir_all(dir.path().join("src")).unwrap();
736 fs::write(&nested_file, "// solidity").unwrap();
737
738 let config = load_lint_config(&nested_file);
739 assert_eq!(config.root, dir.path());
740 assert_eq!(config.ignore_patterns.len(), 1);
741 }
742
743 #[test]
744 fn test_find_git_root() {
745 let dir = tempfile::tempdir().unwrap();
746 fs::create_dir_all(dir.path().join(".git")).unwrap();
748 let nested = dir.path().join("sub/deep");
749 fs::create_dir_all(&nested).unwrap();
750
751 let root = find_git_root(&nested);
752 assert_eq!(root, Some(dir.path().to_path_buf()));
753 }
754
755 #[test]
756 fn test_find_foundry_toml_stops_at_git_boundary() {
757 let dir = tempfile::tempdir().unwrap();
765
766 fs::write(dir.path().join("foundry.toml"), "[profile.default]").unwrap();
768
769 let repo = dir.path().join("repo");
771 fs::create_dir_all(repo.join(".git")).unwrap();
772 fs::create_dir_all(repo.join("sub")).unwrap();
773
774 let found = find_foundry_toml(&repo.join("sub"));
775 assert_eq!(found, None);
777 }
778
779 #[test]
780 fn test_find_foundry_toml_within_git_boundary() {
781 let dir = tempfile::tempdir().unwrap();
789 let repo = dir.path().join("repo");
790 fs::create_dir_all(repo.join(".git")).unwrap();
791 fs::create_dir_all(repo.join("src")).unwrap();
792 let toml_path = repo.join("foundry.toml");
793 fs::write(&toml_path, "[profile.default]").unwrap();
794
795 let found = find_foundry_toml(&repo.join("src"));
796 assert_eq!(found, Some(toml_path));
797 }
798
799 #[test]
800 fn test_find_foundry_toml_no_git_repo_still_walks_up() {
801 let dir = tempfile::tempdir().unwrap();
804 let toml_path = dir.path().join("foundry.toml");
805 fs::write(&toml_path, "[profile.default]").unwrap();
806
807 let nested = dir.path().join("a/b/c");
808 fs::create_dir_all(&nested).unwrap();
809
810 let found = find_foundry_toml(&nested);
811 assert_eq!(found, Some(toml_path));
812 }
813
814 #[test]
817 fn test_load_foundry_config_compiler_settings() {
818 let dir = tempfile::tempdir().unwrap();
819 let toml_path = dir.path().join("foundry.toml");
820 fs::write(
821 &toml_path,
822 r#"
823[profile.default]
824src = "src"
825solc = '0.8.33'
826optimizer = true
827optimizer_runs = 9999999
828via_ir = true
829evm_version = 'osaka'
830ignored_error_codes = [2394, 6321, 3860, 5574, 2424, 8429, 4591]
831"#,
832 )
833 .unwrap();
834
835 let config = load_foundry_config_from_toml(&toml_path);
836 assert_eq!(config.solc_version, Some("0.8.33".to_string()));
837 assert!(config.optimizer);
838 assert_eq!(config.optimizer_runs, 9999999);
839 assert!(config.via_ir);
840 assert_eq!(config.evm_version, Some("osaka".to_string()));
841 assert_eq!(
842 config.ignored_error_codes,
843 vec![2394, 6321, 3860, 5574, 2424, 8429, 4591]
844 );
845 }
846
847 #[test]
848 fn test_load_foundry_config_defaults_when_absent() {
849 let dir = tempfile::tempdir().unwrap();
850 let toml_path = dir.path().join("foundry.toml");
851 fs::write(
852 &toml_path,
853 r#"
854[profile.default]
855src = "src"
856"#,
857 )
858 .unwrap();
859
860 let config = load_foundry_config_from_toml(&toml_path);
861 assert_eq!(config.solc_version, None);
862 assert!(!config.optimizer);
863 assert_eq!(config.optimizer_runs, 200);
864 assert!(!config.via_ir);
865 assert_eq!(config.evm_version, None);
866 assert!(config.ignored_error_codes.is_empty());
867 assert_eq!(config.libs, vec!["lib".to_string()]);
868 }
869
870 #[test]
871 fn test_load_foundry_config_partial_settings() {
872 let dir = tempfile::tempdir().unwrap();
873 let toml_path = dir.path().join("foundry.toml");
874 fs::write(
875 &toml_path,
876 r#"
877[profile.default]
878via_ir = true
879evm_version = "cancun"
880"#,
881 )
882 .unwrap();
883
884 let config = load_foundry_config_from_toml(&toml_path);
885 assert!(config.via_ir);
886 assert!(!config.optimizer); assert_eq!(config.optimizer_runs, 200); assert_eq!(config.evm_version, Some("cancun".to_string()));
889 assert!(config.ignored_error_codes.is_empty());
890 }
891
892 #[test]
893 fn test_load_foundry_config_libs() {
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]
900libs = ["lib", "node_modules", "dependencies"]
901"#,
902 )
903 .unwrap();
904
905 let config = load_foundry_config_from_toml(&toml_path);
906 assert_eq!(
907 config.libs,
908 vec![
909 "lib".to_string(),
910 "node_modules".to_string(),
911 "dependencies".to_string()
912 ]
913 );
914 }
915
916 #[test]
917 fn test_load_foundry_config_libs_defaults_when_absent() {
918 let dir = tempfile::tempdir().unwrap();
919 let toml_path = dir.path().join("foundry.toml");
920 fs::write(
921 &toml_path,
922 r#"
923[profile.default]
924src = "src"
925"#,
926 )
927 .unwrap();
928
929 let config = load_foundry_config_from_toml(&toml_path);
930 assert_eq!(config.libs, vec!["lib".to_string()]);
931 }
932
933 #[test]
934 fn test_load_foundry_config_falls_back_to_default_profile_values() {
935 let dir = tempfile::tempdir().unwrap();
936 let toml_path = dir.path().join("foundry.toml");
937 fs::write(
938 &toml_path,
939 r#"
940[profile.default]
941solc = "0.8.33"
942optimizer_runs = 1234
943libs = ["lib", "node_modules"]
944
945[profile.local]
946src = "contracts"
947"#,
948 )
949 .unwrap();
950
951 let config = load_foundry_config_from_toml_with_profile_name(&toml_path, "local");
952 assert_eq!(config.solc_version, Some("0.8.33".to_string()));
953 assert_eq!(config.optimizer_runs, 1234);
954 assert_eq!(
955 config.libs,
956 vec!["lib".to_string(), "node_modules".to_string()]
957 );
958 assert_eq!(config.sources_dir, "contracts".to_string());
959 }
960
961 #[test]
964 fn test_parse_settings_defaults() {
965 let value = serde_json::json!({});
966 let s = parse_settings(&value);
967 assert!(s.inlay_hints.parameters);
968 assert!(s.inlay_hints.gas_estimates);
969 assert!(s.lint.enabled);
970 assert!(s.file_operations.template_on_create);
971 assert!(s.file_operations.update_imports_on_rename);
972 assert!(s.file_operations.update_imports_on_delete);
973 assert!(s.project_index.full_project_scan);
974 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
975 assert!(!s.project_index.incremental_edit_reindex);
976 assert!(s.lint.severity.is_empty());
977 assert!(s.lint.only.is_empty());
978 assert!(s.lint.exclude.is_empty());
979 }
980
981 #[test]
982 fn test_parse_settings_wrapped() {
983 let value = serde_json::json!({
984 "solidity-language-server": {
985 "inlayHints": { "parameters": false, "gasEstimates": false },
986 "lint": {
987 "enabled": true,
988 "severity": ["high", "med"],
989 "only": ["incorrect-shift"],
990 "exclude": ["pascal-case-struct", "mixed-case-variable"]
991 },
992 "fileOperations": {
993 "templateOnCreate": false,
994 "updateImportsOnRename": false,
995 "updateImportsOnDelete": false
996 },
997 "projectIndex": {
998 "fullProjectScan": true,
999 "cacheMode": "v2",
1000 "incrementalEditReindex": true
1001 },
1002 }
1003 });
1004 let s = parse_settings(&value);
1005 assert!(!s.inlay_hints.parameters);
1006 assert!(!s.inlay_hints.gas_estimates);
1007 assert!(s.lint.enabled);
1008 assert!(!s.file_operations.template_on_create);
1009 assert!(!s.file_operations.update_imports_on_rename);
1010 assert!(!s.file_operations.update_imports_on_delete);
1011 assert!(s.project_index.full_project_scan);
1012 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1013 assert!(s.project_index.incremental_edit_reindex);
1014 assert_eq!(s.lint.severity, vec!["high", "med"]);
1015 assert_eq!(s.lint.only, vec!["incorrect-shift"]);
1016 assert_eq!(
1017 s.lint.exclude,
1018 vec!["pascal-case-struct", "mixed-case-variable"]
1019 );
1020 }
1021
1022 #[test]
1023 fn test_parse_settings_direct() {
1024 let value = serde_json::json!({
1025 "inlayHints": { "parameters": false },
1026 "lint": { "enabled": false },
1027 "fileOperations": {
1028 "templateOnCreate": false,
1029 "updateImportsOnRename": false,
1030 "updateImportsOnDelete": false
1031 },
1032 "projectIndex": {
1033 "fullProjectScan": true,
1034 "cacheMode": "v2",
1035 "incrementalEditReindex": true
1036 }
1037 });
1038 let s = parse_settings(&value);
1039 assert!(!s.inlay_hints.parameters);
1040 assert!(!s.lint.enabled);
1041 assert!(!s.file_operations.template_on_create);
1042 assert!(!s.file_operations.update_imports_on_rename);
1043 assert!(!s.file_operations.update_imports_on_delete);
1044 assert!(s.project_index.full_project_scan);
1045 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1046 assert!(s.project_index.incremental_edit_reindex);
1047 }
1048
1049 #[test]
1050 fn test_parse_settings_partial() {
1051 let value = serde_json::json!({
1052 "solidity-language-server": {
1053 "lint": { "exclude": ["unused-import"] }
1054 }
1055 });
1056 let s = parse_settings(&value);
1057 assert!(s.inlay_hints.parameters);
1059 assert!(s.inlay_hints.gas_estimates);
1060 assert!(s.lint.enabled);
1062 assert!(s.file_operations.template_on_create);
1063 assert!(s.file_operations.update_imports_on_rename);
1064 assert!(s.file_operations.update_imports_on_delete);
1065 assert!(s.project_index.full_project_scan);
1066 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1067 assert!(!s.project_index.incremental_edit_reindex);
1068 assert!(s.lint.severity.is_empty());
1069 assert!(s.lint.only.is_empty());
1070 assert_eq!(s.lint.exclude, vec!["unused-import"]);
1071 }
1072
1073 #[test]
1074 fn test_parse_settings_empty_wrapped() {
1075 let value = serde_json::json!({
1076 "solidity-language-server": {}
1077 });
1078 let s = parse_settings(&value);
1079 assert!(s.inlay_hints.parameters);
1080 assert!(s.inlay_hints.gas_estimates);
1081 assert!(s.lint.enabled);
1082 assert!(s.file_operations.template_on_create);
1083 assert!(s.file_operations.update_imports_on_rename);
1084 assert!(s.file_operations.update_imports_on_delete);
1085 assert!(s.project_index.full_project_scan);
1086 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1087 assert!(!s.project_index.incremental_edit_reindex);
1088 assert!(s.lint.severity.is_empty());
1089 assert!(s.lint.only.is_empty());
1090 assert!(s.lint.exclude.is_empty());
1091 }
1092
1093 #[test]
1094 fn test_parse_settings_project_index_cache_mode_defaults_on_invalid() {
1095 let value = serde_json::json!({
1096 "solidity-language-server": {
1097 "projectIndex": {
1098 "cacheMode": "bad-mode"
1099 }
1100 }
1101 });
1102 let s = parse_settings(&value);
1103 assert_eq!(s.project_index.cache_mode, ProjectIndexCacheMode::V2);
1104 assert!(!s.project_index.incremental_edit_reindex);
1105 }
1106
1107 #[test]
1108 fn test_parse_settings_severity_only() {
1109 let value = serde_json::json!({
1110 "solidity-language-server": {
1111 "lint": {
1112 "severity": ["high", "gas"],
1113 "only": ["incorrect-shift", "asm-keccak256"]
1114 }
1115 }
1116 });
1117 let s = parse_settings(&value);
1118 assert_eq!(s.lint.severity, vec!["high", "gas"]);
1119 assert_eq!(s.lint.only, vec!["incorrect-shift", "asm-keccak256"]);
1120 assert!(s.lint.exclude.is_empty());
1121 }
1122}