solidity_language_server/
config.rs1use 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}
29
30#[derive(Debug, Clone, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct InlayHintsSettings {
34 #[serde(default = "default_true")]
36 pub parameters: bool,
37 #[serde(default = "default_true")]
40 pub gas_estimates: bool,
41}
42
43impl Default for InlayHintsSettings {
44 fn default() -> Self {
45 Self {
46 parameters: true,
47 gas_estimates: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct LintSettings {
56 #[serde(default = "default_true")]
58 pub enabled: bool,
59 #[serde(default)]
63 pub severity: Vec<String>,
64 #[serde(default)]
68 pub only: Vec<String>,
69 #[serde(default)]
72 pub exclude: Vec<String>,
73}
74
75impl Default for LintSettings {
76 fn default() -> Self {
77 Self {
78 enabled: true,
79 severity: Vec::new(),
80 only: Vec::new(),
81 exclude: Vec::new(),
82 }
83 }
84}
85
86fn default_true() -> bool {
87 true
88}
89
90pub fn parse_settings(value: &serde_json::Value) -> Settings {
98 if let Some(inner) = value.get("solidity-language-server")
100 && let Ok(s) = serde_json::from_value::<Settings>(inner.clone())
101 {
102 return s;
103 }
104 serde_json::from_value::<Settings>(value.clone()).unwrap_or_default()
106}
107
108#[derive(Debug, Clone)]
113pub struct FoundryConfig {
114 pub root: PathBuf,
116 pub solc_version: Option<String>,
119 pub remappings: Vec<String>,
122 pub via_ir: bool,
125 pub optimizer: bool,
127 pub optimizer_runs: u64,
130 pub evm_version: Option<String>,
134 pub ignored_error_codes: Vec<u64>,
136 pub sources_dir: String,
138}
139
140impl Default for FoundryConfig {
141 fn default() -> Self {
142 Self {
143 root: PathBuf::new(),
144 solc_version: None,
145 remappings: Vec::new(),
146 via_ir: false,
147 optimizer: false,
148 optimizer_runs: 200,
149 evm_version: None,
150 ignored_error_codes: Vec::new(),
151 sources_dir: "src".to_string(),
152 }
153 }
154}
155
156pub fn load_foundry_config(file_path: &Path) -> FoundryConfig {
158 let toml_path = match find_foundry_toml(file_path) {
159 Some(p) => p,
160 None => return FoundryConfig::default(),
161 };
162 load_foundry_config_from_toml(&toml_path)
163}
164
165pub fn load_foundry_config_from_toml(toml_path: &Path) -> FoundryConfig {
167 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
168
169 let content = match std::fs::read_to_string(toml_path) {
170 Ok(c) => c,
171 Err(_) => {
172 return FoundryConfig {
173 root,
174 ..Default::default()
175 };
176 }
177 };
178
179 let table: toml::Table = match content.parse() {
180 Ok(t) => t,
181 Err(_) => {
182 return FoundryConfig {
183 root,
184 ..Default::default()
185 };
186 }
187 };
188
189 let profile_name = std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string());
190
191 let profile = table
192 .get("profile")
193 .and_then(|p| p.as_table())
194 .and_then(|p| p.get(&profile_name))
195 .and_then(|p| p.as_table());
196
197 let profile = match profile {
198 Some(p) => p,
199 None => {
200 return FoundryConfig {
201 root,
202 ..Default::default()
203 };
204 }
205 };
206
207 let solc_version = profile
209 .get("solc")
210 .or_else(|| profile.get("solc_version"))
211 .and_then(|v| v.as_str())
212 .map(|s| s.to_string());
213
214 let remappings = profile
216 .get("remappings")
217 .and_then(|v| v.as_array())
218 .map(|arr| {
219 arr.iter()
220 .filter_map(|v| v.as_str())
221 .map(|s| s.to_string())
222 .collect()
223 })
224 .unwrap_or_default();
225
226 let via_ir = profile
228 .get("via_ir")
229 .and_then(|v| v.as_bool())
230 .unwrap_or(false);
231
232 let optimizer = profile
234 .get("optimizer")
235 .and_then(|v| v.as_bool())
236 .unwrap_or(false);
237
238 let optimizer_runs = profile
240 .get("optimizer_runs")
241 .and_then(|v| v.as_integer())
242 .map(|v| v as u64)
243 .unwrap_or(200);
244
245 let evm_version = profile
247 .get("evm_version")
248 .and_then(|v| v.as_str())
249 .map(|s| s.to_string());
250
251 let sources_dir = profile
253 .get("src")
254 .and_then(|v| v.as_str())
255 .map(|s| s.to_string())
256 .unwrap_or_else(|| "src".to_string());
257
258 let ignored_error_codes = profile
260 .get("ignored_error_codes")
261 .and_then(|v| v.as_array())
262 .map(|arr| {
263 arr.iter()
264 .filter_map(|v| v.as_integer())
265 .map(|v| v as u64)
266 .collect()
267 })
268 .unwrap_or_default();
269
270 FoundryConfig {
271 root,
272 solc_version,
273 remappings,
274 via_ir,
275 optimizer,
276 optimizer_runs,
277 evm_version,
278 ignored_error_codes,
279 sources_dir,
280 }
281}
282
283#[derive(Debug, Clone)]
285pub struct LintConfig {
286 pub root: PathBuf,
288 pub lint_on_build: bool,
290 pub ignore_patterns: Vec<glob::Pattern>,
292}
293
294impl Default for LintConfig {
295 fn default() -> Self {
296 Self {
297 root: PathBuf::new(),
298 lint_on_build: true,
299 ignore_patterns: Vec::new(),
300 }
301 }
302}
303
304impl LintConfig {
305 pub fn should_lint(&self, file_path: &Path) -> bool {
311 if !self.lint_on_build {
312 return false;
313 }
314
315 if self.ignore_patterns.is_empty() {
316 return true;
317 }
318
319 let relative = file_path.strip_prefix(&self.root).unwrap_or(file_path);
322
323 let rel_str = relative.to_string_lossy();
324
325 for pattern in &self.ignore_patterns {
326 if pattern.matches(&rel_str) {
327 return false;
328 }
329 }
330
331 true
332 }
333}
334
335fn find_git_root(start: &Path) -> Option<PathBuf> {
340 let start = if start.is_file() {
341 start.parent()?
342 } else {
343 start
344 };
345 start
346 .ancestors()
347 .find(|p| p.join(".git").exists())
348 .map(Path::to_path_buf)
349}
350
351pub fn find_foundry_toml(start: &Path) -> Option<PathBuf> {
356 let start_dir = if start.is_file() {
357 start.parent()?
358 } else {
359 start
360 };
361
362 let boundary = find_git_root(start_dir);
363
364 start_dir
365 .ancestors()
366 .take_while(|p| {
368 if let Some(boundary) = &boundary {
369 p.starts_with(boundary)
370 } else {
371 true
372 }
373 })
374 .find(|p| p.join("foundry.toml").is_file())
375 .map(|p| p.join("foundry.toml"))
376}
377
378pub fn load_lint_config(file_path: &Path) -> LintConfig {
382 let toml_path = match find_foundry_toml(file_path) {
383 Some(p) => p,
384 None => return LintConfig::default(),
385 };
386
387 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
388
389 let content = match std::fs::read_to_string(&toml_path) {
390 Ok(c) => c,
391 Err(_) => {
392 return LintConfig {
393 root,
394 ..Default::default()
395 };
396 }
397 };
398
399 let table: toml::Table = match content.parse() {
400 Ok(t) => t,
401 Err(_) => {
402 return LintConfig {
403 root,
404 ..Default::default()
405 };
406 }
407 };
408
409 let profile_name = std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string());
411
412 let lint_table = table
414 .get("profile")
415 .and_then(|p| p.as_table())
416 .and_then(|p| p.get(&profile_name))
417 .and_then(|p| p.as_table())
418 .and_then(|p| p.get("lint"))
419 .and_then(|l| l.as_table());
420
421 let lint_table = match lint_table {
422 Some(t) => t,
423 None => {
424 return LintConfig {
425 root,
426 ..Default::default()
427 };
428 }
429 };
430
431 let lint_on_build = lint_table
433 .get("lint_on_build")
434 .and_then(|v| v.as_bool())
435 .unwrap_or(true);
436
437 let ignore_patterns = lint_table
439 .get("ignore")
440 .and_then(|v| v.as_array())
441 .map(|arr| {
442 arr.iter()
443 .filter_map(|v| v.as_str())
444 .filter_map(|s| glob::Pattern::new(s).ok())
445 .collect()
446 })
447 .unwrap_or_default();
448
449 LintConfig {
450 root,
451 lint_on_build,
452 ignore_patterns,
453 }
454}
455
456pub fn load_lint_config_from_toml(toml_path: &Path) -> LintConfig {
459 let root = toml_path.parent().unwrap_or(Path::new("")).to_path_buf();
460
461 let content = match std::fs::read_to_string(toml_path) {
462 Ok(c) => c,
463 Err(_) => {
464 return LintConfig {
465 root,
466 ..Default::default()
467 };
468 }
469 };
470
471 let table: toml::Table = match content.parse() {
472 Ok(t) => t,
473 Err(_) => {
474 return LintConfig {
475 root,
476 ..Default::default()
477 };
478 }
479 };
480
481 let profile_name = std::env::var("FOUNDRY_PROFILE").unwrap_or_else(|_| "default".to_string());
482
483 let lint_table = table
484 .get("profile")
485 .and_then(|p| p.as_table())
486 .and_then(|p| p.get(&profile_name))
487 .and_then(|p| p.as_table())
488 .and_then(|p| p.get("lint"))
489 .and_then(|l| l.as_table());
490
491 let lint_table = match lint_table {
492 Some(t) => t,
493 None => {
494 return LintConfig {
495 root,
496 ..Default::default()
497 };
498 }
499 };
500
501 let lint_on_build = lint_table
502 .get("lint_on_build")
503 .and_then(|v| v.as_bool())
504 .unwrap_or(true);
505
506 let ignore_patterns = lint_table
507 .get("ignore")
508 .and_then(|v| v.as_array())
509 .map(|arr| {
510 arr.iter()
511 .filter_map(|v| v.as_str())
512 .filter_map(|s| glob::Pattern::new(s).ok())
513 .collect()
514 })
515 .unwrap_or_default();
516
517 LintConfig {
518 root,
519 lint_on_build,
520 ignore_patterns,
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use std::fs;
528
529 #[test]
530 fn test_default_config_lints_everything() {
531 let config = LintConfig::default();
532 assert!(config.should_lint(Path::new("test/MyTest.sol")));
533 assert!(config.should_lint(Path::new("src/Token.sol")));
534 }
535
536 #[test]
537 fn test_lint_on_build_false_skips_all() {
538 let config = LintConfig {
539 lint_on_build: false,
540 ..Default::default()
541 };
542 assert!(!config.should_lint(Path::new("src/Token.sol")));
543 }
544
545 #[test]
546 fn test_ignore_pattern_matches() {
547 let config = LintConfig {
548 root: PathBuf::from("/project"),
549 lint_on_build: true,
550 ignore_patterns: vec![glob::Pattern::new("test/**/*").unwrap()],
551 };
552 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
553 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
554 }
555
556 #[test]
557 fn test_multiple_ignore_patterns() {
558 let config = LintConfig {
559 root: PathBuf::from("/project"),
560 lint_on_build: true,
561 ignore_patterns: vec![
562 glob::Pattern::new("test/**/*").unwrap(),
563 glob::Pattern::new("script/**/*").unwrap(),
564 ],
565 };
566 assert!(!config.should_lint(Path::new("/project/test/MyTest.sol")));
567 assert!(!config.should_lint(Path::new("/project/script/Deploy.sol")));
568 assert!(config.should_lint(Path::new("/project/src/Token.sol")));
569 }
570
571 #[test]
572 fn test_load_lint_config_from_toml() {
573 let dir = tempfile::tempdir().unwrap();
574 let toml_path = dir.path().join("foundry.toml");
575 fs::write(
576 &toml_path,
577 r#"
578[profile.default.lint]
579ignore = ["test/**/*"]
580lint_on_build = true
581"#,
582 )
583 .unwrap();
584
585 let config = load_lint_config_from_toml(&toml_path);
586 assert!(config.lint_on_build);
587 assert_eq!(config.ignore_patterns.len(), 1);
588 assert!(!config.should_lint(&dir.path().join("test/MyTest.sol")));
589 assert!(config.should_lint(&dir.path().join("src/Token.sol")));
590 }
591
592 #[test]
593 fn test_load_lint_config_lint_on_build_false() {
594 let dir = tempfile::tempdir().unwrap();
595 let toml_path = dir.path().join("foundry.toml");
596 fs::write(
597 &toml_path,
598 r#"
599[profile.default.lint]
600lint_on_build = false
601"#,
602 )
603 .unwrap();
604
605 let config = load_lint_config_from_toml(&toml_path);
606 assert!(!config.lint_on_build);
607 assert!(!config.should_lint(&dir.path().join("src/Token.sol")));
608 }
609
610 #[test]
611 fn test_load_lint_config_no_lint_section() {
612 let dir = tempfile::tempdir().unwrap();
613 let toml_path = dir.path().join("foundry.toml");
614 fs::write(
615 &toml_path,
616 r#"
617[profile.default]
618src = "src"
619"#,
620 )
621 .unwrap();
622
623 let config = load_lint_config_from_toml(&toml_path);
624 assert!(config.lint_on_build);
625 assert!(config.ignore_patterns.is_empty());
626 }
627
628 #[test]
629 fn test_find_foundry_toml() {
630 let dir = tempfile::tempdir().unwrap();
631 let toml_path = dir.path().join("foundry.toml");
632 fs::write(&toml_path, "[profile.default]").unwrap();
633
634 let nested = dir.path().join("src");
636 fs::create_dir_all(&nested).unwrap();
637
638 let found = find_foundry_toml(&nested);
639 assert_eq!(found, Some(toml_path));
640 }
641
642 #[test]
643 fn test_load_lint_config_walks_ancestors() {
644 let dir = tempfile::tempdir().unwrap();
645 let toml_path = dir.path().join("foundry.toml");
646 fs::write(
647 &toml_path,
648 r#"
649[profile.default.lint]
650ignore = ["test/**/*"]
651"#,
652 )
653 .unwrap();
654
655 let nested_file = dir.path().join("src/Token.sol");
656 fs::create_dir_all(dir.path().join("src")).unwrap();
657 fs::write(&nested_file, "// solidity").unwrap();
658
659 let config = load_lint_config(&nested_file);
660 assert_eq!(config.root, dir.path());
661 assert_eq!(config.ignore_patterns.len(), 1);
662 }
663
664 #[test]
665 fn test_find_git_root() {
666 let dir = tempfile::tempdir().unwrap();
667 fs::create_dir_all(dir.path().join(".git")).unwrap();
669 let nested = dir.path().join("sub/deep");
670 fs::create_dir_all(&nested).unwrap();
671
672 let root = find_git_root(&nested);
673 assert_eq!(root, Some(dir.path().to_path_buf()));
674 }
675
676 #[test]
677 fn test_find_foundry_toml_stops_at_git_boundary() {
678 let dir = tempfile::tempdir().unwrap();
686
687 fs::write(dir.path().join("foundry.toml"), "[profile.default]").unwrap();
689
690 let repo = dir.path().join("repo");
692 fs::create_dir_all(repo.join(".git")).unwrap();
693 fs::create_dir_all(repo.join("sub")).unwrap();
694
695 let found = find_foundry_toml(&repo.join("sub"));
696 assert_eq!(found, None);
698 }
699
700 #[test]
701 fn test_find_foundry_toml_within_git_boundary() {
702 let dir = tempfile::tempdir().unwrap();
710 let repo = dir.path().join("repo");
711 fs::create_dir_all(repo.join(".git")).unwrap();
712 fs::create_dir_all(repo.join("src")).unwrap();
713 let toml_path = repo.join("foundry.toml");
714 fs::write(&toml_path, "[profile.default]").unwrap();
715
716 let found = find_foundry_toml(&repo.join("src"));
717 assert_eq!(found, Some(toml_path));
718 }
719
720 #[test]
721 fn test_find_foundry_toml_no_git_repo_still_walks_up() {
722 let dir = tempfile::tempdir().unwrap();
725 let toml_path = dir.path().join("foundry.toml");
726 fs::write(&toml_path, "[profile.default]").unwrap();
727
728 let nested = dir.path().join("a/b/c");
729 fs::create_dir_all(&nested).unwrap();
730
731 let found = find_foundry_toml(&nested);
732 assert_eq!(found, Some(toml_path));
733 }
734
735 #[test]
738 fn test_load_foundry_config_compiler_settings() {
739 let dir = tempfile::tempdir().unwrap();
740 let toml_path = dir.path().join("foundry.toml");
741 fs::write(
742 &toml_path,
743 r#"
744[profile.default]
745src = "src"
746solc = '0.8.33'
747optimizer = true
748optimizer_runs = 9999999
749via_ir = true
750evm_version = 'osaka'
751ignored_error_codes = [2394, 6321, 3860, 5574, 2424, 8429, 4591]
752"#,
753 )
754 .unwrap();
755
756 let config = load_foundry_config_from_toml(&toml_path);
757 assert_eq!(config.solc_version, Some("0.8.33".to_string()));
758 assert!(config.optimizer);
759 assert_eq!(config.optimizer_runs, 9999999);
760 assert!(config.via_ir);
761 assert_eq!(config.evm_version, Some("osaka".to_string()));
762 assert_eq!(
763 config.ignored_error_codes,
764 vec![2394, 6321, 3860, 5574, 2424, 8429, 4591]
765 );
766 }
767
768 #[test]
769 fn test_load_foundry_config_defaults_when_absent() {
770 let dir = tempfile::tempdir().unwrap();
771 let toml_path = dir.path().join("foundry.toml");
772 fs::write(
773 &toml_path,
774 r#"
775[profile.default]
776src = "src"
777"#,
778 )
779 .unwrap();
780
781 let config = load_foundry_config_from_toml(&toml_path);
782 assert_eq!(config.solc_version, None);
783 assert!(!config.optimizer);
784 assert_eq!(config.optimizer_runs, 200);
785 assert!(!config.via_ir);
786 assert_eq!(config.evm_version, None);
787 assert!(config.ignored_error_codes.is_empty());
788 }
789
790 #[test]
791 fn test_load_foundry_config_partial_settings() {
792 let dir = tempfile::tempdir().unwrap();
793 let toml_path = dir.path().join("foundry.toml");
794 fs::write(
795 &toml_path,
796 r#"
797[profile.default]
798via_ir = true
799evm_version = "cancun"
800"#,
801 )
802 .unwrap();
803
804 let config = load_foundry_config_from_toml(&toml_path);
805 assert!(config.via_ir);
806 assert!(!config.optimizer); assert_eq!(config.optimizer_runs, 200); assert_eq!(config.evm_version, Some("cancun".to_string()));
809 assert!(config.ignored_error_codes.is_empty());
810 }
811
812 #[test]
815 fn test_parse_settings_defaults() {
816 let value = serde_json::json!({});
817 let s = parse_settings(&value);
818 assert!(s.inlay_hints.parameters);
819 assert!(s.inlay_hints.gas_estimates);
820 assert!(s.lint.enabled);
821 assert!(s.lint.severity.is_empty());
822 assert!(s.lint.only.is_empty());
823 assert!(s.lint.exclude.is_empty());
824 }
825
826 #[test]
827 fn test_parse_settings_wrapped() {
828 let value = serde_json::json!({
829 "solidity-language-server": {
830 "inlayHints": { "parameters": false, "gasEstimates": false },
831 "lint": {
832 "enabled": true,
833 "severity": ["high", "med"],
834 "only": ["incorrect-shift"],
835 "exclude": ["pascal-case-struct", "mixed-case-variable"]
836 }
837 }
838 });
839 let s = parse_settings(&value);
840 assert!(!s.inlay_hints.parameters);
841 assert!(!s.inlay_hints.gas_estimates);
842 assert!(s.lint.enabled);
843 assert_eq!(s.lint.severity, vec!["high", "med"]);
844 assert_eq!(s.lint.only, vec!["incorrect-shift"]);
845 assert_eq!(
846 s.lint.exclude,
847 vec!["pascal-case-struct", "mixed-case-variable"]
848 );
849 }
850
851 #[test]
852 fn test_parse_settings_direct() {
853 let value = serde_json::json!({
854 "inlayHints": { "parameters": false },
855 "lint": { "enabled": false }
856 });
857 let s = parse_settings(&value);
858 assert!(!s.inlay_hints.parameters);
859 assert!(!s.lint.enabled);
860 }
861
862 #[test]
863 fn test_parse_settings_partial() {
864 let value = serde_json::json!({
865 "solidity-language-server": {
866 "lint": { "exclude": ["unused-import"] }
867 }
868 });
869 let s = parse_settings(&value);
870 assert!(s.inlay_hints.parameters);
872 assert!(s.inlay_hints.gas_estimates);
873 assert!(s.lint.enabled);
875 assert!(s.lint.severity.is_empty());
876 assert!(s.lint.only.is_empty());
877 assert_eq!(s.lint.exclude, vec!["unused-import"]);
878 }
879
880 #[test]
881 fn test_parse_settings_empty_wrapped() {
882 let value = serde_json::json!({
883 "solidity-language-server": {}
884 });
885 let s = parse_settings(&value);
886 assert!(s.inlay_hints.parameters);
887 assert!(s.inlay_hints.gas_estimates);
888 assert!(s.lint.enabled);
889 assert!(s.lint.severity.is_empty());
890 assert!(s.lint.only.is_empty());
891 assert!(s.lint.exclude.is_empty());
892 }
893
894 #[test]
895 fn test_parse_settings_severity_only() {
896 let value = serde_json::json!({
897 "solidity-language-server": {
898 "lint": {
899 "severity": ["high", "gas"],
900 "only": ["incorrect-shift", "asm-keccak256"]
901 }
902 }
903 });
904 let s = parse_settings(&value);
905 assert_eq!(s.lint.severity, vec!["high", "gas"]);
906 assert_eq!(s.lint.only, vec!["incorrect-shift", "asm-keccak256"]);
907 assert!(s.lint.exclude.is_empty());
908 }
909}