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")]
193 pub request_timeout_seconds: u64,
194
195 #[serde(default)]
198 pub heuristics: Option<ServerHeuristics>,
199
200 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub name: Option<String>,
209
210 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub handles: Option<Vec<ToolKind>>,
217}
218
219const fn default_timeout() -> u64 {
220 30
221}
222
223const fn default_request_timeout() -> u64 {
224 30
225}
226
227pub const MAX_TIMEOUT_SECONDS: u64 = 900;
249
250impl LspServerConfig {
251 #[must_use]
260 pub fn should_spawn(&self, workspace_root: &Path, max_depth: Option<usize>) -> bool {
261 self.heuristics
262 .as_ref()
263 .is_none_or(|h| h.is_applicable_recursive(workspace_root, max_depth))
264 }
265
266 #[must_use]
272 pub fn id(&self) -> ServerId {
273 self.name
274 .clone()
275 .map_or_else(|| ServerId::from(self.language_id.clone()), ServerId::from)
276 }
277
278 fn builtin(
281 language_id: &str,
282 command: &str,
283 args: &[&str],
284 file_patterns: &[&str],
285 markers: impl IntoIterator<Item = &'static str>,
286 ) -> Self {
287 Self {
288 language_id: language_id.to_string(),
289 command: command.to_string(),
290 args: args.iter().map(ToString::to_string).collect(),
291 env: HashMap::new(),
292 file_patterns: file_patterns.iter().map(ToString::to_string).collect(),
293 initialization_options: None,
294 timeout_seconds: default_timeout(),
295 request_timeout_seconds: default_request_timeout(),
296 heuristics: Some(ServerHeuristics::with_markers(markers)),
297 name: None,
298 handles: None,
299 }
300 }
301
302 #[must_use]
304 pub fn rust_analyzer() -> Self {
305 Self::builtin(
306 "rust",
307 "rust-analyzer",
308 &[],
309 &["**/*.rs"],
310 ["Cargo.toml", "rust-toolchain.toml"],
311 )
312 }
313
314 #[must_use]
316 pub fn pyright() -> Self {
317 Self::builtin(
318 "python",
319 "pyright-langserver",
320 &["--stdio"],
321 &["**/*.py"],
322 [
323 "pyproject.toml",
324 "setup.py",
325 "requirements.txt",
326 "pyrightconfig.json",
327 ],
328 )
329 }
330
331 #[must_use]
333 pub fn typescript() -> Self {
334 Self::builtin(
335 "typescript",
336 "typescript-language-server",
337 &["--stdio"],
338 &["**/*.ts", "**/*.tsx"],
339 ["package.json", "tsconfig.json", "jsconfig.json"],
340 )
341 }
342
343 #[must_use]
345 pub fn gopls() -> Self {
346 Self::builtin(
347 "go",
348 "gopls",
349 &["serve"],
350 &["**/*.go"],
351 ["go.mod", "go.sum"],
352 )
353 }
354
355 #[must_use]
357 pub fn clangd() -> Self {
358 Self::builtin(
359 "cpp",
360 "clangd",
361 &[],
362 &["**/*.c", "**/*.cpp", "**/*.h", "**/*.hpp"],
363 [
364 "CMakeLists.txt",
365 "compile_commands.json",
366 "Makefile",
367 ".clangd",
368 ],
369 )
370 }
371
372 #[must_use]
374 pub fn zls() -> Self {
375 Self::builtin(
376 "zig",
377 "zls",
378 &[],
379 &["**/*.zig"],
380 ["build.zig", "build.zig.zon"],
381 )
382 }
383}
384
385#[cfg(test)]
386#[allow(clippy::unwrap_used)]
387mod tests {
388 use tempfile::TempDir;
389
390 use super::*;
391
392 #[test]
393 fn test_rust_analyzer_defaults() {
394 let config = LspServerConfig::rust_analyzer();
395
396 assert_eq!(config.language_id, "rust");
397 assert_eq!(config.command, "rust-analyzer");
398 assert!(config.args.is_empty());
399 assert!(config.env.is_empty());
400 assert_eq!(config.file_patterns, vec!["**/*.rs"]);
401 assert!(config.initialization_options.is_none());
402 assert_eq!(config.timeout_seconds, 30);
403 }
404
405 #[test]
406 fn test_pyright_defaults() {
407 let config = LspServerConfig::pyright();
408
409 assert_eq!(config.language_id, "python");
410 assert_eq!(config.command, "pyright-langserver");
411 assert_eq!(config.args, vec!["--stdio"]);
412 assert!(config.env.is_empty());
413 assert_eq!(config.file_patterns, vec!["**/*.py"]);
414 assert!(config.initialization_options.is_none());
415 assert_eq!(config.timeout_seconds, 30);
416 }
417
418 #[test]
419 fn test_typescript_defaults() {
420 let config = LspServerConfig::typescript();
421
422 assert_eq!(config.language_id, "typescript");
423 assert_eq!(config.command, "typescript-language-server");
424 assert_eq!(config.args, vec!["--stdio"]);
425 assert!(config.env.is_empty());
426 assert_eq!(config.file_patterns, vec!["**/*.ts", "**/*.tsx"]);
427 assert!(config.initialization_options.is_none());
428 assert_eq!(config.timeout_seconds, 30);
429 }
430
431 #[test]
432 fn test_default_timeout() {
433 assert_eq!(default_timeout(), 30);
434 }
435
436 #[test]
437 fn test_custom_config() {
438 let mut env = HashMap::new();
439 env.insert("RUST_LOG".to_string(), "debug".to_string());
440
441 let config = LspServerConfig {
442 language_id: "custom".to_string(),
443 command: "custom-lsp".to_string(),
444 args: vec!["--flag".to_string()],
445 env: env.clone(),
446 file_patterns: vec!["**/*.custom".to_string()],
447 initialization_options: Some(serde_json::json!({"key": "value"})),
448 timeout_seconds: 60,
449 request_timeout_seconds: 45,
450 heuristics: None,
451 name: None,
452 handles: None,
453 };
454
455 assert_eq!(config.language_id, "custom");
456 assert_eq!(config.command, "custom-lsp");
457 assert_eq!(config.args, vec!["--flag"]);
458 assert_eq!(config.env.get("RUST_LOG"), Some(&"debug".to_string()));
459 assert_eq!(config.file_patterns, vec!["**/*.custom"]);
460 assert!(config.initialization_options.is_some());
461 assert_eq!(config.timeout_seconds, 60);
462 }
463
464 #[test]
465 fn test_serde_roundtrip() {
466 let original = LspServerConfig::rust_analyzer();
467
468 let serialized = serde_json::to_string(&original).unwrap();
469 let deserialized: LspServerConfig = serde_json::from_str(&serialized).unwrap();
470
471 assert_eq!(deserialized.language_id, original.language_id);
472 assert_eq!(deserialized.command, original.command);
473 assert_eq!(deserialized.args, original.args);
474 assert_eq!(deserialized.timeout_seconds, original.timeout_seconds);
475 assert_eq!(
476 deserialized.request_timeout_seconds,
477 original.request_timeout_seconds
478 );
479 }
480
481 #[test]
482 fn test_default_request_timeout() {
483 assert_eq!(default_request_timeout(), 30);
484 }
485
486 #[test]
487 fn test_clone() {
488 let config = LspServerConfig::rust_analyzer();
489 let cloned = config.clone();
490
491 assert_eq!(cloned.language_id, config.language_id);
492 assert_eq!(cloned.command, config.command);
493 assert_eq!(cloned.timeout_seconds, config.timeout_seconds);
494 }
495
496 #[test]
497 fn test_empty_env() {
498 let config = LspServerConfig::rust_analyzer();
499 assert!(config.env.is_empty());
500 }
501
502 #[test]
503 fn test_multiple_file_patterns() {
504 let config = LspServerConfig::typescript();
505 assert_eq!(config.file_patterns.len(), 2);
506 assert!(config.file_patterns.contains(&"**/*.ts".to_string()));
507 assert!(config.file_patterns.contains(&"**/*.tsx".to_string()));
508 }
509
510 #[test]
511 fn test_initialization_options_none_by_default() {
512 let configs = vec![
513 LspServerConfig::rust_analyzer(),
514 LspServerConfig::pyright(),
515 LspServerConfig::typescript(),
516 ];
517
518 for config in configs {
519 assert!(config.initialization_options.is_none());
520 }
521 }
522
523 #[test]
525 fn test_heuristics_empty_always_applicable() {
526 let heuristics = ServerHeuristics::default();
527 let tmp = TempDir::new().unwrap();
528 assert!(heuristics.is_applicable(tmp.path()));
529 }
530
531 #[test]
532 fn test_heuristics_marker_present() {
533 let tmp = TempDir::new().unwrap();
534 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
535
536 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
537 assert!(heuristics.is_applicable(tmp.path()));
538 }
539
540 #[test]
541 fn test_heuristics_marker_absent() {
542 let tmp = TempDir::new().unwrap();
543 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
544 assert!(!heuristics.is_applicable(tmp.path()));
545 }
546
547 #[test]
548 fn test_heuristics_any_marker_matches() {
549 let tmp = TempDir::new().unwrap();
550 std::fs::write(tmp.path().join("setup.py"), "").unwrap();
551
552 let heuristics =
553 ServerHeuristics::with_markers(["pyproject.toml", "setup.py", "requirements.txt"]);
554 assert!(heuristics.is_applicable(tmp.path()));
555 }
556
557 #[test]
558 fn test_should_spawn_without_heuristics() {
559 let config = LspServerConfig {
560 language_id: "test".to_string(),
561 command: "test-lsp".to_string(),
562 args: vec![],
563 env: HashMap::new(),
564 file_patterns: vec![],
565 initialization_options: None,
566 timeout_seconds: 30,
567 request_timeout_seconds: 30,
568 heuristics: None,
569 name: None,
570 handles: None,
571 };
572
573 let tmp = TempDir::new().unwrap();
574 assert!(config.should_spawn(tmp.path(), None));
575 }
576
577 #[test]
578 fn test_should_spawn_with_heuristics() {
579 let tmp = TempDir::new().unwrap();
580 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
581
582 let config = LspServerConfig::rust_analyzer();
583 assert!(config.should_spawn(tmp.path(), None));
584 }
585
586 #[test]
587 fn test_should_not_spawn_without_markers() {
588 let tmp = TempDir::new().unwrap();
589 let config = LspServerConfig::rust_analyzer();
590 assert!(!config.should_spawn(tmp.path(), None));
591 }
592
593 #[test]
594 fn test_heuristics_serde_roundtrip() {
595 let heuristics = ServerHeuristics::with_markers(["Cargo.toml", "rust-toolchain.toml"]);
596 let json = serde_json::to_string(&heuristics).unwrap();
597 let deserialized: ServerHeuristics = serde_json::from_str(&json).unwrap();
598 assert_eq!(deserialized.project_markers, heuristics.project_markers);
599 }
600
601 #[test]
602 fn test_default_rust_analyzer_heuristics() {
603 let config = LspServerConfig::rust_analyzer();
604 assert!(config.heuristics.is_some());
605 let markers = &config.heuristics.unwrap().project_markers;
606 assert!(markers.contains(&"Cargo.toml".to_string()));
607 }
608
609 #[test]
610 fn test_gopls_defaults() {
611 let config = LspServerConfig::gopls();
612
613 assert_eq!(config.language_id, "go");
614 assert_eq!(config.command, "gopls");
615 assert_eq!(config.args, vec!["serve"]);
616 assert!(config.heuristics.is_some());
617 let markers = &config.heuristics.unwrap().project_markers;
618 assert!(markers.contains(&"go.mod".to_string()));
619 assert!(markers.contains(&"go.sum".to_string()));
620 }
621
622 #[test]
623 fn test_clangd_defaults() {
624 let config = LspServerConfig::clangd();
625
626 assert_eq!(config.language_id, "cpp");
627 assert_eq!(config.command, "clangd");
628 assert!(config.args.is_empty());
629 assert!(config.heuristics.is_some());
630 let markers = &config.heuristics.unwrap().project_markers;
631 assert!(markers.contains(&"CMakeLists.txt".to_string()));
632 assert!(markers.contains(&"compile_commands.json".to_string()));
633 }
634
635 #[test]
636 fn test_zls_defaults() {
637 let config = LspServerConfig::zls();
638
639 assert_eq!(config.language_id, "zig");
640 assert_eq!(config.command, "zls");
641 assert!(config.args.is_empty());
642 assert!(config.heuristics.is_some());
643 let markers = &config.heuristics.unwrap().project_markers;
644 assert!(markers.contains(&"build.zig".to_string()));
645 assert!(markers.contains(&"build.zig.zon".to_string()));
646 }
647
648 #[test]
650 fn test_recursive_empty_markers_always_applicable() {
651 let heuristics = ServerHeuristics::default();
652 let tmp = TempDir::new().unwrap();
653 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
654 }
655
656 #[test]
657 fn test_recursive_marker_at_root() {
658 let tmp = TempDir::new().unwrap();
659 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
660
661 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
662 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
663 }
664
665 #[test]
666 fn test_recursive_nested_python_project() {
667 let tmp = TempDir::new().unwrap();
668 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
670 let python_dir = tmp.path().join("python");
672 std::fs::create_dir(&python_dir).unwrap();
673 std::fs::write(python_dir.join("pyproject.toml"), "").unwrap();
674
675 let heuristics = ServerHeuristics::with_markers(["pyproject.toml", "setup.py"]);
676 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
677 }
678
679 #[test]
680 fn test_recursive_deeply_nested_marker() {
681 let tmp = TempDir::new().unwrap();
682 let deep_path = tmp.path().join("level1").join("level2").join("level3");
684 std::fs::create_dir_all(&deep_path).unwrap();
685 std::fs::write(deep_path.join("go.mod"), "").unwrap();
686
687 let heuristics = ServerHeuristics::with_markers(["go.mod"]);
688 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
689 }
690
691 #[test]
692 fn test_recursive_no_marker_found() {
693 let tmp = TempDir::new().unwrap();
694 std::fs::create_dir(tmp.path().join("src")).unwrap();
695 std::fs::write(tmp.path().join("src").join("main.rs"), "").unwrap();
696
697 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
698 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
699 }
700
701 #[test]
702 fn test_recursive_max_depth_respected() {
703 let tmp = TempDir::new().unwrap();
704 let deep_path = tmp.path().join("a").join("b").join("c").join("d").join("e");
706 std::fs::create_dir_all(&deep_path).unwrap();
707 std::fs::write(deep_path.join("Cargo.toml"), "").unwrap();
708
709 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
710 assert!(!heuristics.is_applicable_recursive(tmp.path(), Some(3)));
712 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
714 }
715
716 #[test]
717 fn test_recursive_excludes_node_modules() {
718 let tmp = TempDir::new().unwrap();
719 let node_modules = tmp.path().join("node_modules").join("some-package");
721 std::fs::create_dir_all(&node_modules).unwrap();
722 std::fs::write(node_modules.join("package.json"), "").unwrap();
723
724 let heuristics = ServerHeuristics::with_markers(["package.json"]);
725 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
726 }
727
728 #[test]
729 fn test_recursive_excludes_target_directory() {
730 let tmp = TempDir::new().unwrap();
731 let target = tmp.path().join("target").join("debug");
733 std::fs::create_dir_all(&target).unwrap();
734 std::fs::write(target.join("Cargo.toml"), "").unwrap();
735
736 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
737 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
738 }
739
740 #[test]
741 fn test_recursive_excludes_git_directory() {
742 let tmp = TempDir::new().unwrap();
743 let git_dir = tmp.path().join(".git").join("hooks");
744 std::fs::create_dir_all(&git_dir).unwrap();
745 std::fs::write(git_dir.join("Cargo.toml"), "").unwrap();
746
747 let heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
748 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
749 }
750
751 #[test]
752 fn test_recursive_excludes_pycache() {
753 let tmp = TempDir::new().unwrap();
754 let pycache = tmp.path().join("__pycache__");
755 std::fs::create_dir_all(&pycache).unwrap();
756 std::fs::write(pycache.join("pyproject.toml"), "").unwrap();
757
758 let heuristics = ServerHeuristics::with_markers(["pyproject.toml"]);
759 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
760 }
761
762 #[test]
763 fn test_recursive_excludes_venv() {
764 let tmp = TempDir::new().unwrap();
765 let venv = tmp.path().join(".venv").join("lib");
766 std::fs::create_dir_all(&venv).unwrap();
767 std::fs::write(venv.join("setup.py"), "").unwrap();
768
769 let heuristics = ServerHeuristics::with_markers(["setup.py"]);
770 assert!(!heuristics.is_applicable_recursive(tmp.path(), None));
771 }
772
773 #[test]
774 fn test_recursive_finds_marker_outside_excluded() {
775 let tmp = TempDir::new().unwrap();
776 let node_modules = tmp.path().join("node_modules");
778 std::fs::create_dir_all(&node_modules).unwrap();
779 std::fs::write(node_modules.join("package.json"), "").unwrap();
780 let src = tmp.path().join("src");
782 std::fs::create_dir_all(&src).unwrap();
783 std::fs::write(src.join("package.json"), "").unwrap();
784
785 let heuristics = ServerHeuristics::with_markers(["package.json"]);
786 assert!(heuristics.is_applicable_recursive(tmp.path(), None));
787 }
788
789 #[test]
790 fn test_recursive_monorepo_structure() {
791 let tmp = TempDir::new().unwrap();
792 let rust_pkg = tmp.path().join("packages").join("rust-lib");
794 let python_pkg = tmp.path().join("packages").join("python-bindings");
795 let ts_pkg = tmp.path().join("packages").join("typescript-client");
796
797 std::fs::create_dir_all(&rust_pkg).unwrap();
798 std::fs::create_dir_all(&python_pkg).unwrap();
799 std::fs::create_dir_all(&ts_pkg).unwrap();
800
801 std::fs::write(rust_pkg.join("Cargo.toml"), "").unwrap();
802 std::fs::write(python_pkg.join("pyproject.toml"), "").unwrap();
803 std::fs::write(ts_pkg.join("package.json"), "").unwrap();
804
805 let rust_heuristics = ServerHeuristics::with_markers(["Cargo.toml"]);
807 let python_heuristics = ServerHeuristics::with_markers(["pyproject.toml"]);
808 let ts_heuristics = ServerHeuristics::with_markers(["package.json"]);
809
810 assert!(rust_heuristics.is_applicable_recursive(tmp.path(), None));
811 assert!(python_heuristics.is_applicable_recursive(tmp.path(), None));
812 assert!(ts_heuristics.is_applicable_recursive(tmp.path(), None));
813 }
814
815 #[test]
816 fn test_should_spawn_recursive() {
817 let tmp = TempDir::new().unwrap();
818 let python_dir = tmp.path().join("bindings").join("python");
820 std::fs::create_dir_all(&python_dir).unwrap();
821 std::fs::write(python_dir.join("pyproject.toml"), "").unwrap();
822
823 let config = LspServerConfig::pyright();
824 assert!(config.should_spawn(tmp.path(), None));
825 }
826
827 #[test]
828 fn test_should_spawn_with_custom_max_depth() {
829 let tmp = TempDir::new().unwrap();
830 let deep_path = tmp.path().join("a").join("b").join("c").join("d");
831 std::fs::create_dir_all(&deep_path).unwrap();
832 std::fs::write(deep_path.join("Cargo.toml"), "").unwrap();
833
834 let config = LspServerConfig::rust_analyzer();
835 assert!(!config.should_spawn(tmp.path(), Some(2)));
837 assert!(config.should_spawn(tmp.path(), None));
839 }
840
841 #[test]
842 fn test_default_heuristics_max_depth() {
843 assert_eq!(DEFAULT_HEURISTICS_MAX_DEPTH, 10);
844 }
845
846 #[test]
847 fn test_excluded_directories_constant() {
848 assert!(EXCLUDED_DIRECTORIES.contains(&"node_modules"));
849 assert!(EXCLUDED_DIRECTORIES.contains(&"target"));
850 assert!(EXCLUDED_DIRECTORIES.contains(&".git"));
851 assert!(EXCLUDED_DIRECTORIES.contains(&"__pycache__"));
852 assert!(EXCLUDED_DIRECTORIES.contains(&".venv"));
853 }
854}