1use std::collections::HashMap;
4use std::path::Path;
5
6use ignore::WalkBuilder;
7use serde::{Deserialize, Serialize};
8
9use super::routing::{ServerId, ToolKind};
10
11pub const DEFAULT_HEURISTICS_MAX_DEPTH: usize = 10;
13
14const EXCLUDED_DIRECTORIES: &[&str] = &[
17 "node_modules",
18 "target",
19 ".git",
20 "__pycache__",
21 ".venv",
22 "venv",
23 ".tox",
24 ".mypy_cache",
25 ".pytest_cache",
26 "build",
27 "dist",
28 ".cargo",
29 ".rustup",
30 "vendor",
31 "coverage",
32 ".next",
33 ".nuxt",
34];
35
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41#[serde(deny_unknown_fields)]
42pub struct ServerHeuristics {
43 #[serde(default)]
51 pub project_markers: Vec<String>,
52}
53
54impl ServerHeuristics {
55 #[must_use]
57 pub fn with_markers<I, S>(markers: I) -> Self
58 where
59 I: IntoIterator<Item = S>,
60 S: Into<String>,
61 {
62 Self {
63 project_markers: markers.into_iter().map(Into::into).collect(),
64 }
65 }
66
67 #[must_use]
73 pub fn is_applicable(&self, workspace_root: &Path) -> bool {
74 if self.project_markers.is_empty() {
75 return true;
76 }
77 self.project_markers
78 .iter()
79 .any(|marker| workspace_root.join(marker).exists())
80 }
81
82 #[must_use]
96 pub fn is_applicable_recursive(&self, workspace_root: &Path, max_depth: Option<usize>) -> bool {
97 if self.project_markers.is_empty() {
98 return true;
99 }
100
101 if self.is_applicable(workspace_root) {
103 return true;
104 }
105
106 let depth = max_depth.unwrap_or(DEFAULT_HEURISTICS_MAX_DEPTH);
107 self.find_any_marker_recursive(workspace_root, depth)
108 }
109
110 fn find_any_marker_recursive(&self, workspace_root: &Path, max_depth: usize) -> bool {
112 let mut builder = WalkBuilder::new(workspace_root);
113 builder
114 .max_depth(Some(max_depth))
115 .hidden(false)
116 .git_ignore(true)
117 .git_global(false)
118 .git_exclude(false)
119 .follow_links(false)
120 .standard_filters(false)
121 .filter_entry(|entry| {
122 if entry.file_type().is_some_and(|ft| ft.is_dir())
124 && let Some(name) = entry.file_name().to_str()
125 && EXCLUDED_DIRECTORIES.contains(&name)
126 {
127 return false;
128 }
129 true
130 });
131
132 for entry in builder.build().flatten() {
133 let path = entry.path();
134
135 if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
137 && self.project_markers.iter().any(|m| m == file_name)
138 {
139 return true;
140 }
141 }
142
143 false
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct LspServerConfig {
151 pub language_id: String,
153
154 pub command: String,
156
157 #[serde(default)]
159 pub args: Vec<String>,
160
161 #[serde(default)]
163 pub env: HashMap<String, String>,
164
165 #[serde(default)]
167 pub file_patterns: Vec<String>,
168
169 #[serde(default)]
171 pub initialization_options: Option<serde_json::Value>,
172
173 #[serde(default = "default_timeout")]
179 pub timeout_seconds: u64,
180
181 #[serde(default = "default_request_timeout")]
191 pub request_timeout_seconds: u64,
192
193 #[serde(default)]
196 pub heuristics: Option<ServerHeuristics>,
197
198 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub name: Option<String>,
207
208 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub handles: Option<Vec<ToolKind>>,
215}
216
217const fn default_timeout() -> u64 {
218 30
219}
220
221const fn default_request_timeout() -> u64 {
222 30
223}
224
225pub const MAX_TIMEOUT_SECONDS: u64 = 900;
246
247impl LspServerConfig {
248 #[must_use]
257 pub fn should_spawn(&self, workspace_root: &Path, max_depth: Option<usize>) -> bool {
258 self.heuristics
259 .as_ref()
260 .is_none_or(|h| h.is_applicable_recursive(workspace_root, max_depth))
261 }
262
263 #[must_use]
269 pub fn id(&self) -> ServerId {
270 self.name
271 .clone()
272 .map_or_else(|| ServerId::from(self.language_id.clone()), ServerId::from)
273 }
274
275 fn builtin(
278 language_id: &str,
279 command: &str,
280 args: &[&str],
281 file_patterns: &[&str],
282 markers: impl IntoIterator<Item = &'static str>,
283 ) -> Self {
284 Self {
285 language_id: language_id.to_string(),
286 command: command.to_string(),
287 args: args.iter().map(ToString::to_string).collect(),
288 env: HashMap::new(),
289 file_patterns: file_patterns.iter().map(ToString::to_string).collect(),
290 initialization_options: None,
291 timeout_seconds: default_timeout(),
292 request_timeout_seconds: default_request_timeout(),
293 heuristics: Some(ServerHeuristics::with_markers(markers)),
294 name: None,
295 handles: None,
296 }
297 }
298
299 #[must_use]
301 pub fn rust_analyzer() -> Self {
302 Self::builtin(
303 "rust",
304 "rust-analyzer",
305 &[],
306 &["**/*.rs"],
307 ["Cargo.toml", "rust-toolchain.toml"],
308 )
309 }
310
311 #[must_use]
313 pub fn pyright() -> Self {
314 Self::builtin(
315 "python",
316 "pyright-langserver",
317 &["--stdio"],
318 &["**/*.py"],
319 [
320 "pyproject.toml",
321 "setup.py",
322 "requirements.txt",
323 "pyrightconfig.json",
324 ],
325 )
326 }
327
328 #[must_use]
330 pub fn typescript() -> Self {
331 Self::builtin(
332 "typescript",
333 "typescript-language-server",
334 &["--stdio"],
335 &["**/*.ts", "**/*.tsx"],
336 ["package.json", "tsconfig.json", "jsconfig.json"],
337 )
338 }
339
340 #[must_use]
342 pub fn gopls() -> Self {
343 Self::builtin(
344 "go",
345 "gopls",
346 &["serve"],
347 &["**/*.go"],
348 ["go.mod", "go.sum"],
349 )
350 }
351
352 #[must_use]
354 pub fn clangd() -> Self {
355 Self::builtin(
356 "cpp",
357 "clangd",
358 &[],
359 &["**/*.c", "**/*.cpp", "**/*.h", "**/*.hpp"],
360 [
361 "CMakeLists.txt",
362 "compile_commands.json",
363 "Makefile",
364 ".clangd",
365 ],
366 )
367 }
368
369 #[must_use]
371 pub fn zls() -> Self {
372 Self::builtin(
373 "zig",
374 "zls",
375 &[],
376 &["**/*.zig"],
377 ["build.zig", "build.zig.zon"],
378 )
379 }
380}
381
382#[cfg(test)]
383#[allow(clippy::unwrap_used)]
384mod tests {
385 use tempfile::TempDir;
386
387 use super::*;
388
389 #[test]
390 fn test_rust_analyzer_defaults() {
391 let config = LspServerConfig::rust_analyzer();
392
393 assert_eq!(config.language_id, "rust");
394 assert_eq!(config.command, "rust-analyzer");
395 assert!(config.args.is_empty());
396 assert!(config.env.is_empty());
397 assert_eq!(config.file_patterns, vec!["**/*.rs"]);
398 assert!(config.initialization_options.is_none());
399 assert_eq!(config.timeout_seconds, 30);
400 }
401
402 #[test]
403 fn test_pyright_defaults() {
404 let config = LspServerConfig::pyright();
405
406 assert_eq!(config.language_id, "python");
407 assert_eq!(config.command, "pyright-langserver");
408 assert_eq!(config.args, vec!["--stdio"]);
409 assert!(config.env.is_empty());
410 assert_eq!(config.file_patterns, vec!["**/*.py"]);
411 assert!(config.initialization_options.is_none());
412 assert_eq!(config.timeout_seconds, 30);
413 }
414
415 #[test]
416 fn test_typescript_defaults() {
417 let config = LspServerConfig::typescript();
418
419 assert_eq!(config.language_id, "typescript");
420 assert_eq!(config.command, "typescript-language-server");
421 assert_eq!(config.args, vec!["--stdio"]);
422 assert!(config.env.is_empty());
423 assert_eq!(config.file_patterns, vec!["**/*.ts", "**/*.tsx"]);
424 assert!(config.initialization_options.is_none());
425 assert_eq!(config.timeout_seconds, 30);
426 }
427
428 #[test]
429 fn test_default_timeout() {
430 assert_eq!(default_timeout(), 30);
431 }
432
433 #[test]
434 fn test_custom_config() {
435 let mut env = HashMap::new();
436 env.insert("RUST_LOG".to_string(), "debug".to_string());
437
438 let config = LspServerConfig {
439 language_id: "custom".to_string(),
440 command: "custom-lsp".to_string(),
441 args: vec!["--flag".to_string()],
442 env: env.clone(),
443 file_patterns: vec!["**/*.custom".to_string()],
444 initialization_options: Some(serde_json::json!({"key": "value"})),
445 timeout_seconds: 60,
446 request_timeout_seconds: 45,
447 heuristics: None,
448 name: None,
449 handles: None,
450 };
451
452 assert_eq!(config.language_id, "custom");
453 assert_eq!(config.command, "custom-lsp");
454 assert_eq!(config.args, vec!["--flag"]);
455 assert_eq!(config.env.get("RUST_LOG"), Some(&"debug".to_string()));
456 assert_eq!(config.file_patterns, vec!["**/*.custom"]);
457 assert!(config.initialization_options.is_some());
458 assert_eq!(config.timeout_seconds, 60);
459 }
460
461 #[test]
462 fn test_serde_roundtrip() {
463 let original = LspServerConfig::rust_analyzer();
464
465 let serialized = serde_json::to_string(&original).unwrap();
466 let deserialized: LspServerConfig = serde_json::from_str(&serialized).unwrap();
467
468 assert_eq!(deserialized.language_id, original.language_id);
469 assert_eq!(deserialized.command, original.command);
470 assert_eq!(deserialized.args, original.args);
471 assert_eq!(deserialized.timeout_seconds, original.timeout_seconds);
472 assert_eq!(
473 deserialized.request_timeout_seconds,
474 original.request_timeout_seconds
475 );
476 }
477
478 #[test]
479 fn test_default_request_timeout() {
480 assert_eq!(default_request_timeout(), 30);
481 }
482
483 #[test]
484 fn test_clone() {
485 let config = LspServerConfig::rust_analyzer();
486 let cloned = config.clone();
487
488 assert_eq!(cloned.language_id, config.language_id);
489 assert_eq!(cloned.command, config.command);
490 assert_eq!(cloned.timeout_seconds, config.timeout_seconds);
491 }
492
493 #[test]
494 fn test_empty_env() {
495 let config = LspServerConfig::rust_analyzer();
496 assert!(config.env.is_empty());
497 }
498
499 #[test]
500 fn test_multiple_file_patterns() {
501 let config = LspServerConfig::typescript();
502 assert_eq!(config.file_patterns.len(), 2);
503 assert!(config.file_patterns.contains(&"**/*.ts".to_string()));
504 assert!(config.file_patterns.contains(&"**/*.tsx".to_string()));
505 }
506
507 #[test]
508 fn test_initialization_options_none_by_default() {
509 let configs = vec![
510 LspServerConfig::rust_analyzer(),
511 LspServerConfig::pyright(),
512 LspServerConfig::typescript(),
513 ];
514
515 for config in configs {
516 assert!(config.initialization_options.is_none());
517 }
518 }
519
520 #[test]
522 fn test_heuristics_empty_always_applicable() {
523 let heuristics = ServerHeuristics::default();
524 let tmp = TempDir::new().unwrap();
525 assert!(heuristics.is_applicable(tmp.path()));
526 }
527
528 #[test]
529 fn test_heuristics_marker_present() {
530 let tmp = TempDir::new().unwrap();
531 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
532
533 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
534 assert!(heuristics.is_applicable(tmp.path()));
535 }
536
537 #[test]
538 fn test_heuristics_marker_absent() {
539 let tmp = TempDir::new().unwrap();
540 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
541 assert!(!heuristics.is_applicable(tmp.path()));
542 }
543
544 #[test]
545 fn test_heuristics_any_marker_matches() {
546 let tmp = TempDir::new().unwrap();
547 std::fs::write(tmp.path().join("setup.py"), "").unwrap();
548
549 let heuristics =
550 ServerHeuristics::with_markers(["pyproject.toml", "setup.py", "requirements.txt"]);
551 assert!(heuristics.is_applicable(tmp.path()));
552 }
553
554 #[test]
555 fn test_should_spawn_without_heuristics() {
556 let config = LspServerConfig {
557 language_id: "test".to_string(),
558 command: "test-lsp".to_string(),
559 args: vec![],
560 env: HashMap::new(),
561 file_patterns: vec![],
562 initialization_options: None,
563 timeout_seconds: 30,
564 request_timeout_seconds: 30,
565 heuristics: None,
566 name: None,
567 handles: None,
568 };
569
570 let tmp = TempDir::new().unwrap();
571 assert!(config.should_spawn(tmp.path(), None));
572 }
573
574 #[test]
575 fn test_should_spawn_with_heuristics() {
576 let tmp = TempDir::new().unwrap();
577 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
578
579 let config = LspServerConfig::rust_analyzer();
580 assert!(config.should_spawn(tmp.path(), None));
581 }
582
583 #[test]
584 fn test_should_not_spawn_without_markers() {
585 let tmp = TempDir::new().unwrap();
586 let config = LspServerConfig::rust_analyzer();
587 assert!(!config.should_spawn(tmp.path(), None));
588 }
589
590 #[test]
591 fn test_heuristics_serde_roundtrip() {
592 let heuristics = ServerHeuristics::with_markers(["Cargo.toml", "rust-toolchain.toml"]);
593 let json = serde_json::to_string(&heuristics).unwrap();
594 let deserialized: ServerHeuristics = serde_json::from_str(&json).unwrap();
595 assert_eq!(deserialized.project_markers, heuristics.project_markers);
596 }
597
598 #[test]
599 fn test_default_rust_analyzer_heuristics() {
600 let config = LspServerConfig::rust_analyzer();
601 assert!(config.heuristics.is_some());
602 let markers = &config.heuristics.unwrap().project_markers;
603 assert!(markers.contains(&"Cargo.toml".to_string()));
604 }
605
606 #[test]
607 fn test_gopls_defaults() {
608 let config = LspServerConfig::gopls();
609
610 assert_eq!(config.language_id, "go");
611 assert_eq!(config.command, "gopls");
612 assert_eq!(config.args, vec!["serve"]);
613 assert!(config.heuristics.is_some());
614 let markers = &config.heuristics.unwrap().project_markers;
615 assert!(markers.contains(&"go.mod".to_string()));
616 assert!(markers.contains(&"go.sum".to_string()));
617 }
618
619 #[test]
620 fn test_clangd_defaults() {
621 let config = LspServerConfig::clangd();
622
623 assert_eq!(config.language_id, "cpp");
624 assert_eq!(config.command, "clangd");
625 assert!(config.args.is_empty());
626 assert!(config.heuristics.is_some());
627 let markers = &config.heuristics.unwrap().project_markers;
628 assert!(markers.contains(&"CMakeLists.txt".to_string()));
629 assert!(markers.contains(&"compile_commands.json".to_string()));
630 }
631
632 #[test]
633 fn test_zls_defaults() {
634 let config = LspServerConfig::zls();
635
636 assert_eq!(config.language_id, "zig");
637 assert_eq!(config.command, "zls");
638 assert!(config.args.is_empty());
639 assert!(config.heuristics.is_some());
640 let markers = &config.heuristics.unwrap().project_markers;
641 assert!(markers.contains(&"build.zig".to_string()));
642 assert!(markers.contains(&"build.zig.zon".to_string()));
643 }
644
645 #[test]
647 fn test_recursive_empty_markers_always_applicable() {
648 let heuristics = ServerHeuristics::default();
649 let tmp = TempDir::new().unwrap();
650 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
651 }
652
653 #[test]
654 fn test_recursive_marker_at_root() {
655 let tmp = TempDir::new().unwrap();
656 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
657
658 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
659 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
660 }
661
662 #[test]
663 fn test_recursive_nested_python_project() {
664 let tmp = TempDir::new().unwrap();
665 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
667 let python_dir = tmp.path().join("python");
669 std::fs::create_dir(&python_dir).unwrap();
670 std::fs::write(python_dir.join("pyproject.toml"), "").unwrap();
671
672 let heuristics = ServerHeuristics::with_markers(["pyproject.toml", "setup.py"]);
673 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
674 }
675
676 #[test]
677 fn test_recursive_deeply_nested_marker() {
678 let tmp = TempDir::new().unwrap();
679 let deep_path = tmp.path().join("level1").join("level2").join("level3");
681 std::fs::create_dir_all(&deep_path).unwrap();
682 std::fs::write(deep_path.join("go.mod"), "").unwrap();
683
684 let heuristics = ServerHeuristics::with_markers(["go.mod"]);
685 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
686 }
687
688 #[test]
689 fn test_recursive_no_marker_found() {
690 let tmp = TempDir::new().unwrap();
691 std::fs::create_dir(tmp.path().join("src")).unwrap();
692 std::fs::write(tmp.path().join("src").join("main.rs"), "").unwrap();
693
694 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
695 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
696 }
697
698 #[test]
699 fn test_recursive_max_depth_respected() {
700 let tmp = TempDir::new().unwrap();
701 let deep_path = tmp.path().join("a").join("b").join("c").join("d").join("e");
703 std::fs::create_dir_all(&deep_path).unwrap();
704 std::fs::write(deep_path.join("Cargo.toml"), "").unwrap();
705
706 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
707 assert!(!heuristics.is_applicable_recursive(tmp.path(), Some(3)));
709 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
711 }
712
713 #[test]
714 fn test_recursive_excludes_node_modules() {
715 let tmp = TempDir::new().unwrap();
716 let node_modules = tmp.path().join("node_modules").join("some-package");
718 std::fs::create_dir_all(&node_modules).unwrap();
719 std::fs::write(node_modules.join("package.json"), "").unwrap();
720
721 let heuristics = ServerHeuristics::with_markers(["package.json"]);
722 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
723 }
724
725 #[test]
726 fn test_recursive_excludes_target_directory() {
727 let tmp = TempDir::new().unwrap();
728 let target = tmp.path().join("target").join("debug");
730 std::fs::create_dir_all(&target).unwrap();
731 std::fs::write(target.join("Cargo.toml"), "").unwrap();
732
733 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
734 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
735 }
736
737 #[test]
738 fn test_recursive_excludes_git_directory() {
739 let tmp = TempDir::new().unwrap();
740 let git_dir = tmp.path().join(".git").join("hooks");
741 std::fs::create_dir_all(&git_dir).unwrap();
742 std::fs::write(git_dir.join("Cargo.toml"), "").unwrap();
743
744 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
745 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
746 }
747
748 #[test]
749 fn test_recursive_excludes_pycache() {
750 let tmp = TempDir::new().unwrap();
751 let pycache = tmp.path().join("__pycache__");
752 std::fs::create_dir_all(&pycache).unwrap();
753 std::fs::write(pycache.join("pyproject.toml"), "").unwrap();
754
755 let heuristics = ServerHeuristics::with_markers(["pyproject.toml"]);
756 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
757 }
758
759 #[test]
760 fn test_recursive_excludes_venv() {
761 let tmp = TempDir::new().unwrap();
762 let venv = tmp.path().join(".venv").join("lib");
763 std::fs::create_dir_all(&venv).unwrap();
764 std::fs::write(venv.join("setup.py"), "").unwrap();
765
766 let heuristics = ServerHeuristics::with_markers(["setup.py"]);
767 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
768 }
769
770 #[test]
771 fn test_recursive_finds_marker_outside_excluded() {
772 let tmp = TempDir::new().unwrap();
773 let node_modules = tmp.path().join("node_modules");
775 std::fs::create_dir_all(&node_modules).unwrap();
776 std::fs::write(node_modules.join("package.json"), "").unwrap();
777 let src = tmp.path().join("src");
779 std::fs::create_dir_all(&src).unwrap();
780 std::fs::write(src.join("package.json"), "").unwrap();
781
782 let heuristics = ServerHeuristics::with_markers(["package.json"]);
783 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
784 }
785
786 #[test]
787 fn test_recursive_monorepo_structure() {
788 let tmp = TempDir::new().unwrap();
789 let rust_pkg = tmp.path().join("packages").join("rust-lib");
791 let python_pkg = tmp.path().join("packages").join("python-bindings");
792 let ts_pkg = tmp.path().join("packages").join("typescript-client");
793
794 std::fs::create_dir_all(&rust_pkg).unwrap();
795 std::fs::create_dir_all(&python_pkg).unwrap();
796 std::fs::create_dir_all(&ts_pkg).unwrap();
797
798 std::fs::write(rust_pkg.join("Cargo.toml"), "").unwrap();
799 std::fs::write(python_pkg.join("pyproject.toml"), "").unwrap();
800 std::fs::write(ts_pkg.join("package.json"), "").unwrap();
801
802 let rust_heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
804 let python_heuristics = ServerHeuristics::with_markers(["pyproject.toml"]);
805 let ts_heuristics = ServerHeuristics::with_markers(["package.json"]);
806
807 assert!(rust_heuristics.is_applicable_recursive(tmp.path(), None));
808 assert!(python_heuristics.is_applicable_recursive(tmp.path(), None));
809 assert!(ts_heuristics.is_applicable_recursive(tmp.path(), None));
810 }
811
812 #[test]
813 fn test_should_spawn_recursive() {
814 let tmp = TempDir::new().unwrap();
815 let python_dir = tmp.path().join("bindings").join("python");
817 std::fs::create_dir_all(&python_dir).unwrap();
818 std::fs::write(python_dir.join("pyproject.toml"), "").unwrap();
819
820 let config = LspServerConfig::pyright();
821 assert!(config.should_spawn(tmp.path(), None));
822 }
823
824 #[test]
825 fn test_should_spawn_with_custom_max_depth() {
826 let tmp = TempDir::new().unwrap();
827 let deep_path = tmp.path().join("a").join("b").join("c").join("d");
828 std::fs::create_dir_all(&deep_path).unwrap();
829 std::fs::write(deep_path.join("Cargo.toml"), "").unwrap();
830
831 let config = LspServerConfig::rust_analyzer();
832 assert!(!config.should_spawn(tmp.path(), Some(2)));
834 assert!(config.should_spawn(tmp.path(), None));
836 }
837
838 #[test]
839 fn test_default_heuristics_max_depth() {
840 assert_eq!(DEFAULT_HEURISTICS_MAX_DEPTH, 10);
841 }
842
843 #[test]
844 fn test_excluded_directories_constant() {
845 assert!(EXCLUDED_DIRECTORIES.contains(&"node_modules"));
846 assert!(EXCLUDED_DIRECTORIES.contains(&"target"));
847 assert!(EXCLUDED_DIRECTORIES.contains(&".git"));
848 assert!(EXCLUDED_DIRECTORIES.contains(&"__pycache__"));
849 assert!(EXCLUDED_DIRECTORIES.contains(&".venv"));
850 }
851}