1use std::borrow::Cow;
7use std::ffi::{OsStr, OsString};
8use std::path::{Path, PathBuf};
9
10use rustc_hash::{FxHashMap, FxHashSet};
11
12use fallow_config::{ExternalPluginDef, PackageJson, PluginDetection, UsedClassMemberRule};
13
14use crate::discover::SOURCE_EXTENSIONS;
15
16use super::super::{PathRule, Plugin, PluginResult, PluginUsedExportRule, UsedExportRule};
17use super::{AggregatedPluginResult, PluginRegexValidationError, PluginRegexValidationErrorInput};
18
19#[must_use]
28pub fn is_source_ext_root_pattern(pat: &str) -> bool {
29 if pat.is_empty() || pat.contains('/') {
30 return false;
31 }
32 for expanded in expand_brace_pattern(pat) {
33 if expanded.starts_with('.') {
34 return false;
35 }
36 let Some(ext) = std::path::Path::new(&expanded).extension() else {
37 return false;
38 };
39 let Some(ext_str) = ext.to_str() else {
40 return false;
41 };
42 if !SOURCE_EXTENSIONS.contains(&ext_str) {
43 return false;
44 }
45 }
46 true
47}
48
49#[must_use]
51pub fn prepare_config_pattern(pat: &str) -> Cow<'_, str> {
52 if is_source_ext_root_pattern(pat) {
53 Cow::Owned(format!("**/{pat}"))
54 } else {
55 Cow::Borrowed(pat)
56 }
57}
58
59pub fn process_static_patterns(
61 plugin: &dyn Plugin,
62 root: &Path,
63 result: &mut AggregatedPluginResult,
64) {
65 let pname = plugin.name().to_string();
66 result.active_plugins.push(pname.clone());
67 result
68 .entry_point_roles
69 .insert(pname.clone(), plugin.entry_point_role());
70
71 collect_static_plugin_rules(plugin, &pname, result);
72 collect_static_plugin_metadata(plugin, root, result);
73}
74
75fn collect_static_plugin_rules(
78 plugin: &dyn Plugin,
79 pname: &str,
80 result: &mut AggregatedPluginResult,
81) {
82 for rule in plugin.entry_pattern_rules() {
83 result.entry_patterns.push((rule, pname.to_string()));
84 }
85 for pat in plugin.config_patterns() {
86 result.config_patterns.push((*pat).to_string());
87 }
88 for pat in plugin.always_used() {
89 result
90 .always_used
91 .push(((*pat).to_string(), pname.to_string()));
92 }
93 for rule in plugin.used_export_rules() {
94 result
95 .used_exports
96 .push(PluginUsedExportRule::new(pname.to_string(), rule));
97 }
98 for member in plugin.used_class_members() {
99 result
100 .used_class_members
101 .push(UsedClassMemberRule::from(*member));
102 }
103 for rule in plugin.used_class_member_rules() {
104 result.used_class_members.push(rule);
105 }
106 result
107 .framework_class_member_contracts
108 .extend(plugin.framework_class_member_contracts());
109 for pat in plugin.fixture_glob_patterns() {
110 result
111 .fixture_patterns
112 .push(((*pat).to_string(), pname.to_string()));
113 }
114}
115
116fn collect_static_plugin_metadata(
119 plugin: &dyn Plugin,
120 root: &Path,
121 result: &mut AggregatedPluginResult,
122) {
123 for dep in plugin.tooling_dependencies() {
124 result.tooling_dependencies.push((*dep).to_string());
125 }
126 for prefix in plugin.virtual_module_prefixes() {
127 result.virtual_module_prefixes.push((*prefix).to_string());
128 }
129 for suffix in plugin.virtual_package_suffixes() {
130 result.virtual_package_suffixes.push((*suffix).to_string());
131 }
132 for pattern in plugin.generated_import_patterns() {
133 result
134 .generated_import_patterns
135 .push((*pattern).to_string());
136 }
137 for prefix in plugin.generated_type_import_prefixes() {
138 result
139 .generated_type_import_prefixes
140 .push((*prefix).to_string());
141 }
142 for (prefix, replacement) in plugin.path_aliases(root) {
143 result.path_aliases.push((prefix.to_string(), replacement));
144 }
145 result
146 .framework_static_dir_mappings
147 .extend(plugin.static_dir_mappings(root));
148 result
149 .auto_imports
150 .extend(plugin.auto_imports(root).into_iter().map(|mut rule| {
151 if rule.scope.is_empty() {
152 rule.scope.push(root.to_path_buf());
153 }
154 rule
155 }));
156 result
157 .provided_dependencies
158 .extend(plugin.provided_dependencies());
159}
160
161pub fn process_package_json_metadata(
163 active: &[&dyn Plugin],
164 pkg: &PackageJson,
165 root: &Path,
166 result: &mut AggregatedPluginResult,
167 regex_errors: &mut Vec<PluginRegexValidationError>,
168) {
169 for plugin in active {
170 let package_referenced = plugin.package_json_referenced_dependencies(pkg, root);
171 if !package_referenced.is_empty() {
172 let pkg_path = root.join("package.json");
173 result.package_referenced_dependencies.extend(
174 package_referenced
175 .into_iter()
176 .map(|dep| (pkg_path.clone(), dep)),
177 );
178 }
179 let plugin_result = plugin.resolve_package_json(pkg, root);
180 if plugin_result.is_empty() {
181 continue;
182 }
183 tracing::debug!(
184 plugin = plugin.name(),
185 deps = plugin_result.referenced_dependencies.len(),
186 "resolved package.json metadata"
187 );
188 if let Err(mut errors) = process_config_result(plugin.name(), plugin_result, result, None) {
189 regex_errors.append(&mut errors);
190 }
191 }
192}
193
194pub fn is_external_plugin_active(
199 ext: &ExternalPluginDef,
200 all_deps: &[String],
201 root: &Path,
202 discovered_files: &[PathBuf],
203) -> bool {
204 if let Some(detection) = &ext.detection {
205 let all_dep_refs: Vec<&str> = all_deps.iter().map(String::as_str).collect();
206 check_plugin_detection(detection, &all_dep_refs, root, discovered_files)
207 } else if !ext.enablers.is_empty() {
208 ext.enablers.iter().any(|enabler| {
209 if enabler.ends_with('/') {
210 all_deps.iter().any(|d| d.starts_with(enabler))
211 } else {
212 all_deps.iter().any(|d| d == enabler)
213 }
214 })
215 } else {
216 false
217 }
218}
219
220pub fn process_external_plugins(
222 external_plugins: &[ExternalPluginDef],
223 all_deps: &[String],
224 root: &Path,
225 discovered_files: &[PathBuf],
226 result: &mut AggregatedPluginResult,
227) {
228 for ext in external_plugins {
229 let is_active = is_external_plugin_active(ext, all_deps, root, discovered_files);
230 if is_active {
231 result.active_plugins.push(ext.name.clone());
232 result
233 .entry_point_roles
234 .insert(ext.name.clone(), ext.entry_point_role);
235 result.entry_patterns.extend(
236 ext.entry_points
237 .iter()
238 .map(|p| (PathRule::new(p.clone()), ext.name.clone())),
239 );
240 if !ext.manifest_entries.is_empty() {
241 result.entry_patterns.extend(
242 crate::plugins::manifest_entries::evaluate_manifest_entries(ext, root)
243 .into_iter()
244 .map(|rule| (rule, ext.name.clone())),
245 );
246 }
247 result.config_patterns.extend(ext.config_patterns.clone());
248 result.always_used.extend(
249 ext.config_patterns
250 .iter()
251 .chain(ext.always_used.iter())
252 .map(|p| (p.clone(), ext.name.clone())),
253 );
254 result
255 .tooling_dependencies
256 .extend(ext.tooling_dependencies.clone());
257 for ue in &ext.used_exports {
258 result.used_exports.push(PluginUsedExportRule::new(
259 ext.name.clone(),
260 UsedExportRule::new(ue.pattern.clone(), ue.exports.clone()),
261 ));
262 }
263 result
264 .used_class_members
265 .extend(ext.used_class_members.iter().cloned());
266 }
267 }
268}
269
270pub struct ConfigCandidateIndex {
282 dirs: FxHashMap<PathBuf, FxHashSet<OsString>>,
283}
284
285impl ConfigCandidateIndex {
286 #[must_use]
290 pub(crate) fn build<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Self {
291 let mut dirs: FxHashMap<PathBuf, FxHashSet<OsString>> = FxHashMap::default();
292 for path in paths {
293 if let (Some(parent), Some(name)) = (path.parent(), path.file_name()) {
294 dirs.entry(parent.to_path_buf())
295 .or_default()
296 .insert(name.to_os_string());
297 }
298 }
299 Self { dirs }
300 }
301
302 #[must_use]
304 pub(crate) fn contains_file(&self, path: &Path) -> bool {
305 match (path.parent(), path.file_name()) {
306 (Some(dir), Some(name)) => self.dir_contains(dir, name),
307 _ => false,
308 }
309 }
310
311 #[must_use]
315 pub(crate) fn dir_contains(&self, dir: &Path, name: &OsStr) -> bool {
316 self.dirs.get(dir).is_some_and(|names| names.contains(name))
317 }
318
319 #[must_use]
324 pub(crate) fn any_descendant_contains(&self, root: &Path, name: &OsStr) -> bool {
325 self.dirs
326 .iter()
327 .any(|(dir, names)| dir.starts_with(root) && names.contains(name))
328 }
329
330 #[must_use]
336 pub(crate) fn any_descendant_matches(
337 &self,
338 root: &Path,
339 matcher: &globset::GlobMatcher,
340 ) -> bool {
341 self.dirs.iter().any(|(dir, names)| {
342 dir.starts_with(root) && names.iter().any(|name| matcher.is_match(Path::new(name)))
343 })
344 }
345
346 pub(super) fn names_in_dir(&self, dir: &Path) -> Option<&FxHashSet<OsString>> {
348 self.dirs.get(dir)
349 }
350
351 pub(super) fn glob_matches_in_dir(
352 &self,
353 dir: &Path,
354 matcher: &globset::GlobMatcher,
355 ) -> Vec<PathBuf> {
356 self.dirs.get(dir).map_or_else(Vec::new, |names| {
357 names
358 .iter()
359 .filter(|name| matcher.is_match(Path::new(name)))
360 .map(|name| dir.join(name))
361 .collect()
362 })
363 }
364}
365
366pub fn discover_config_files<'a>(
386 config_matchers: &[(&'a dyn Plugin, Vec<globset::GlobMatcher>)],
387 resolved_plugins: &FxHashSet<&str>,
388 roots: &[&Path],
389 production_mode: bool,
390 candidate_index: Option<&ConfigCandidateIndex>,
391) -> Vec<(PathBuf, &'a dyn Plugin)> {
392 use rayon::prelude::*;
393 let mut pending: Vec<(&'a dyn Plugin, &Path, String, bool)> = Vec::new();
394 for (plugin, _) in config_matchers {
395 let resolved = resolved_plugins.contains(plugin.name());
400 for root in roots {
401 for pat in plugin.config_patterns() {
402 if !production_mode && is_source_ext_root_pattern(pat) {
403 continue;
404 }
405 if resolved && !pattern_needs_filesystem(pat) {
406 continue;
407 }
408 pending.push((*plugin, *root, pat.to_string(), resolved));
409 }
410 }
411 }
412
413 let hits: Vec<(PathBuf, &'a dyn Plugin)> = pending
414 .par_iter()
415 .flat_map_iter(|(plugin, root, pat, resolved)| {
416 expand_brace_pattern(pat)
417 .into_iter()
418 .flat_map(|expanded| match candidate_index {
419 Some(index) if !pattern_needs_filesystem(&expanded) => {
425 match_pattern_in_index(root, &expanded, index)
426 }
427 _ => discover_pattern_matches(root, &expanded),
428 })
429 .filter(|path| {
432 !*resolved || !candidate_index.is_some_and(|index| index.contains_file(path))
433 })
434 .map(move |path| (path, *plugin))
435 .collect::<Vec<_>>()
436 })
437 .collect();
438
439 let mut seen: FxHashSet<(PathBuf, &'a str)> = FxHashSet::default();
440 let mut config_files: Vec<(PathBuf, &'a dyn Plugin)> = Vec::with_capacity(hits.len());
441 for (path, plugin) in hits {
442 if seen.insert((path.clone(), plugin.name())) {
443 config_files.push((path, plugin));
444 }
445 }
446 config_files
447}
448
449fn pattern_has_glob(pattern: &str) -> bool {
450 pattern.contains('*') || pattern.contains('?') || pattern.contains('[')
451}
452
453pub(super) fn pattern_needs_filesystem(pattern: &str) -> bool {
460 let mut components = pattern.split('/').peekable();
461 let mut needs_fs = false;
462 while let Some(component) = components.next() {
463 if components.peek().is_none() {
464 break; }
466 if component.starts_with('.')
467 && component != "."
468 && component != ".."
469 && !crate::discover::is_allowed_hidden_dir(OsStr::new(component))
470 {
471 needs_fs = true;
472 break;
473 }
474 if is_default_ignored_directory(component) {
475 needs_fs = true;
476 break;
477 }
478 }
479 needs_fs
480}
481
482fn is_default_ignored_directory(name: &str) -> bool {
485 fallow_config::DEFAULT_IGNORE_PATTERNS
486 .iter()
487 .any(|pattern| {
488 pattern
489 .strip_prefix("**/")
490 .and_then(|rest| rest.strip_suffix("/**"))
491 .is_some_and(|directory| directory == name)
492 })
493}
494
495fn match_pattern_in_index(
500 root: &Path,
501 pattern: &str,
502 index: &ConfigCandidateIndex,
503) -> Vec<PathBuf> {
504 if !pattern_has_glob(pattern) {
505 let path = root.join(pattern);
506 return match (path.parent(), path.file_name()) {
507 (Some(dir), Some(name)) if index.dir_contains(dir, name) => vec![path],
508 _ => Vec::new(),
509 };
510 }
511
512 if let Some(stripped) = pattern.strip_prefix("**/") {
513 return match_pattern_in_index(root, stripped, index);
514 }
515
516 let (dir, file_pattern) = match pattern.rsplit_once('/') {
517 Some((parent, file_pattern)) if !pattern_has_glob(parent) => {
518 (root.join(parent), file_pattern)
519 }
520 Some(_) => return Vec::new(),
521 None => (root.to_path_buf(), pattern),
522 };
523
524 let Ok(matcher) = globset::Glob::new(file_pattern).map(|g| g.compile_matcher()) else {
525 return Vec::new();
526 };
527 index.glob_matches_in_dir(&dir, &matcher)
528}
529
530fn discover_pattern_matches(root: &Path, pattern: &str) -> Vec<PathBuf> {
531 if !pattern_has_glob(pattern) {
532 let path = root.join(pattern);
533 return if path.is_file() {
534 vec![path]
535 } else {
536 Vec::new()
537 };
538 }
539
540 if let Some(stripped) = pattern.strip_prefix("**/") {
541 return discover_pattern_matches(root, stripped);
542 }
543
544 let (dir, file_pattern) = match pattern.rsplit_once('/') {
545 Some((parent, file_pattern)) if !pattern_has_glob(parent) => {
546 (root.join(parent), file_pattern)
547 }
548 Some(_) => return Vec::new(),
549 None => (root.to_path_buf(), pattern),
550 };
551
552 scan_dir_for_pattern(&dir, file_pattern)
553}
554
555fn scan_dir_for_pattern(dir: &Path, file_pattern: &str) -> Vec<PathBuf> {
556 let Ok(matcher) = globset::Glob::new(file_pattern).map(|g| g.compile_matcher()) else {
557 return Vec::new();
558 };
559 let Ok(entries) = std::fs::read_dir(dir) else {
560 return Vec::new();
561 };
562
563 entries
564 .filter_map(Result::ok)
565 .map(|entry| entry.path())
566 .filter(|path| path.is_file())
567 .filter(|path| {
568 path.file_name()
569 .is_some_and(|name| matcher.is_match(std::path::Path::new(name)))
570 })
571 .collect()
572}
573
574pub(super) fn expand_brace_pattern(pattern: &str) -> Vec<String> {
575 let Some(open) = pattern.find('{') else {
576 return vec![pattern.to_string()];
577 };
578 let Some(close_rel) = pattern[open + 1..].find('}') else {
579 return vec![pattern.to_string()];
580 };
581 let close = open + 1 + close_rel;
582
583 let prefix = &pattern[..open];
584 let suffix = &pattern[close + 1..];
585 let inner = &pattern[open + 1..close];
586 let mut expanded = Vec::new();
587 for option in inner.split(',') {
588 for tail in expand_brace_pattern(suffix) {
589 expanded.push(format!("{prefix}{option}{tail}"));
590 }
591 }
592 expanded
593}
594
595fn collect_path_rule_regex_errors(
600 rule: &crate::plugins::PathRule,
601 plugin_name: &str,
602 config_path: Option<&Path>,
603 rule_kind: &'static str,
604 errors: &mut Vec<PluginRegexValidationError>,
605) {
606 for pattern in &rule.exclude_regexes {
607 if let Err(source) = regex::Regex::new(pattern) {
608 errors.push(PluginRegexValidationError::new(
609 PluginRegexValidationErrorInput {
610 plugin_name,
611 config_path,
612 rule_kind,
613 field: "exclude_regexes",
614 rule_pattern: &rule.pattern,
615 regex_pattern: pattern,
616 source: &source,
617 },
618 ));
619 }
620 }
621 for pattern in &rule.exclude_segment_regexes {
622 if let Err(source) = regex::Regex::new(pattern) {
623 errors.push(PluginRegexValidationError::new(
624 PluginRegexValidationErrorInput {
625 plugin_name,
626 config_path,
627 rule_kind,
628 field: "exclude_segment_regexes",
629 rule_pattern: &rule.pattern,
630 regex_pattern: pattern,
631 source: &source,
632 },
633 ));
634 }
635 }
636}
637
638pub fn process_config_result(
644 plugin_name: &str,
645 plugin_result: PluginResult,
646 result: &mut AggregatedPluginResult,
647 config_path: Option<&Path>,
648) -> Result<(), Vec<PluginRegexValidationError>> {
649 let mut regex_errors = Vec::new();
650
651 for rule in &plugin_result.entry_patterns {
652 collect_path_rule_regex_errors(
653 rule,
654 plugin_name,
655 config_path,
656 "entry_patterns[]",
657 &mut regex_errors,
658 );
659 }
660 for rule in &plugin_result.used_exports {
661 collect_path_rule_regex_errors(
662 &rule.path,
663 plugin_name,
664 config_path,
665 "used_exports[].path",
666 &mut regex_errors,
667 );
668 }
669 if !regex_errors.is_empty() {
670 return Err(regex_errors);
671 }
672 merge_plugin_result_fields(plugin_name, plugin_result, result);
673 Ok(())
674}
675
676fn merge_plugin_result_fields(
680 pname: &str,
681 plugin_result: PluginResult,
682 result: &mut AggregatedPluginResult,
683) {
684 if plugin_result.replace_entry_patterns && !plugin_result.entry_patterns.is_empty() {
685 let first_replacement = result.replaced_entry_plugins.insert(pname.to_string());
686 if first_replacement || !plugin_result.accumulate_config_entry_patterns {
687 result.entry_patterns.retain(|(_, name)| name != pname);
688 }
689 }
690 if plugin_result.replace_used_export_rules && !plugin_result.used_exports.is_empty() {
691 result.used_exports.retain(|rule| rule.plugin_name != pname);
692 }
693 result.entry_patterns.extend(
694 plugin_result
695 .entry_patterns
696 .into_iter()
697 .map(|rule| (rule, pname.to_string())),
698 );
699 result.used_exports.extend(
700 plugin_result
701 .used_exports
702 .into_iter()
703 .map(|rule| PluginUsedExportRule::new(pname.to_string(), rule)),
704 );
705 result
706 .used_class_members
707 .extend(plugin_result.used_class_members);
708 result
709 .referenced_dependencies
710 .extend(plugin_result.referenced_dependencies);
711 result
712 .package_referenced_dependencies
713 .extend(plugin_result.package_referenced_dependencies);
714 result.discovered_always_used.extend(
715 plugin_result
716 .always_used_files
717 .into_iter()
718 .map(|p| (p, pname.to_string())),
719 );
720 for (prefix, replacement) in plugin_result.path_aliases {
721 result
722 .path_aliases
723 .retain(|(existing_prefix, _)| existing_prefix != &prefix);
724 result.path_aliases.push((prefix, replacement));
725 }
726 result.setup_files.extend(
727 plugin_result
728 .setup_files
729 .into_iter()
730 .map(|p| (p, pname.to_string())),
731 );
732 result.fixture_patterns.extend(
733 plugin_result
734 .fixture_patterns
735 .into_iter()
736 .map(|p| (p, pname.to_string())),
737 );
738 result
739 .scss_include_paths
740 .extend(plugin_result.scss_include_paths);
741 result
742 .static_dir_mappings
743 .extend(plugin_result.static_dir_mappings);
744 result
745 .framework_static_dir_mappings
746 .extend(plugin_result.framework_static_dir_mappings);
747 result
748 .provided_dependencies
749 .extend(plugin_result.provided_dependencies);
750 result
751 .federation_sources
752 .extend(plugin_result.federation_sources);
753 for diagnostic in plugin_result.config_diagnostics {
754 if !result.config_diagnostics.contains(&diagnostic) {
755 result.config_diagnostics.push(diagnostic);
756 }
757 }
758}
759
760pub fn check_has_config_file(
762 plugin: &dyn Plugin,
763 config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
764 relative_files: &[(PathBuf, String)],
765) -> bool {
766 !plugin.config_patterns().is_empty()
767 && config_matchers.iter().any(|(p, matchers)| {
768 p.name() == plugin.name()
769 && relative_files
770 .iter()
771 .any(|(_, rel)| matchers.iter().any(|m| m.is_match(rel.as_str())))
772 })
773}
774
775pub fn check_plugin_detection(
777 detection: &PluginDetection,
778 all_deps: &[&str],
779 root: &Path,
780 discovered_files: &[PathBuf],
781) -> bool {
782 match detection {
783 PluginDetection::Dependency { package } => all_deps.iter().any(|d| *d == package),
784 PluginDetection::FileExists { pattern } => {
785 if let Ok(matcher) = globset::Glob::new(pattern).map(|g| g.compile_matcher()) {
786 for file in discovered_files {
787 let relative = file.strip_prefix(root).unwrap_or(file);
788 if matcher.is_match(relative) {
789 return true;
790 }
791 }
792 }
793 let full_pattern = root.join(pattern).to_string_lossy().to_string();
794 glob::glob(&full_pattern).is_ok_and(|mut g| g.next().is_some())
795 }
796 PluginDetection::All { conditions } => conditions
797 .iter()
798 .all(|c| check_plugin_detection(c, all_deps, root, discovered_files)),
799 PluginDetection::Any { conditions } => conditions
800 .iter()
801 .any(|c| check_plugin_detection(c, all_deps, root, discovered_files)),
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn pattern_needs_filesystem_only_for_non_allowlisted_hidden_dirs() {
811 assert!(pattern_needs_filesystem(".config/prisma.ts"));
814 assert!(!pattern_needs_filesystem("tsconfig.json"));
817 assert!(!pattern_needs_filesystem("prisma/schema.prisma"));
818 assert!(!pattern_needs_filesystem(".eslintrc.json"));
819 assert!(!pattern_needs_filesystem("**/project.json"));
820 assert!(!pattern_needs_filesystem(".storybook/main.ts"));
821 assert!(!pattern_needs_filesystem("a/b/c.json"));
822 assert!(pattern_needs_filesystem("build/webpack.prod.js"));
824 assert!(!pattern_needs_filesystem("config/webpack.prod.js"));
825 }
826
827 #[test]
828 fn config_candidate_index_matches_plain_nested_and_glob_shapes() {
829 let root = Path::new("/project");
830 let index = ConfigCandidateIndex::build([
831 Path::new("/project/tsconfig.json"),
832 Path::new("/project/packages/a/tsconfig.json"),
833 Path::new("/project/prisma/schema.prisma"),
834 Path::new("/project/src/main.ts"),
835 ]);
836
837 assert_eq!(
839 match_pattern_in_index(root, "tsconfig.json", &index),
840 vec![PathBuf::from("/project/tsconfig.json")]
841 );
842 assert_eq!(
843 match_pattern_in_index(Path::new("/project/packages/a"), "tsconfig.json", &index),
844 vec![PathBuf::from("/project/packages/a/tsconfig.json")]
845 );
846 assert_eq!(
848 match_pattern_in_index(root, "prisma/schema.prisma", &index),
849 vec![PathBuf::from("/project/prisma/schema.prisma")]
850 );
851 assert_eq!(
852 match_pattern_in_index(root, "**/tsconfig.json", &index),
853 vec![PathBuf::from("/project/tsconfig.json")]
854 );
855 assert_eq!(
856 match_pattern_in_index(Path::new("/project/prisma"), "*.prisma", &index),
857 vec![PathBuf::from("/project/prisma/schema.prisma")]
858 );
859 assert!(match_pattern_in_index(root, "missing.json", &index).is_empty());
861 }
862}