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