1use std::collections::{BTreeMap, BTreeSet};
4
5use anyhow::{Context, Result, bail};
6use qk_config::{Target, Workspace};
7use qk_graph::ProjectGraph;
8use serde::Serialize;
9use serde_json::{Map, Value};
10
11#[derive(Clone, Debug)]
12pub struct Request {
13 pub project: String,
14 pub target: String,
15 pub configuration: Option<String>,
16 pub requested_configuration: Option<String>,
18 pub args: Vec<String>,
19}
20
21impl Request {
22 pub fn parse(value: &str) -> Result<Self> {
23 let parts: Vec<_> = value.split(':').collect();
24 if !(2..=3).contains(&parts.len()) || parts.iter().any(|part| part.is_empty()) {
25 bail!("expected project:target[:configuration], got {value:?}");
26 }
27 Ok(Self {
28 project: parts[0].into(),
29 target: parts[1].into(),
30 configuration: parts.get(2).map(|part| (*part).into()),
31 requested_configuration: parts.get(2).map(|part| (*part).into()),
32 args: Vec::new(),
33 })
34 }
35
36 pub fn parse_in(workspace: &Workspace, value: &str) -> Result<Self> {
38 let Some((project, identifier)) = value.split_once(':') else {
39 return Self::parse(value);
40 };
41 if let Some(project_config) = workspace.projects.get(project) {
42 if project_config.targets.contains_key(identifier) {
43 return Ok(Self {
44 project: project.into(),
45 target: identifier.into(),
46 configuration: None,
47 requested_configuration: None,
48 args: Vec::new(),
49 });
50 }
51 if let Some((target, configuration)) = identifier.rsplit_once(':')
52 && project_config.targets.contains_key(target)
53 && !configuration.is_empty()
54 {
55 return Ok(Self {
56 project: project.into(),
57 target: target.into(),
58 configuration: Some(configuration.into()),
59 requested_configuration: Some(configuration.into()),
60 args: Vec::new(),
61 });
62 }
63 }
64 Self::parse(value)
65 }
66}
67
68#[derive(Clone, Debug, Serialize)]
69pub struct Task {
70 pub id: String,
71 pub project: String,
72 pub target: String,
73 pub configuration: Option<String>,
74 pub args: Vec<String>,
75 pub definition: Target,
76 pub dependencies: BTreeSet<String>,
77 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
81 pub edges: BTreeMap<String, Edge>,
82}
83
84impl Task {
85 pub fn edge(&self, dependency: &str) -> Edge {
87 self.edges.get(dependency).copied().unwrap_or_default()
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Edge {
95 pub inputs: bool,
99 pub wait_for: WaitFor,
100}
101
102impl Default for Edge {
103 fn default() -> Self {
104 Self {
105 inputs: true,
106 wait_for: WaitFor::Started,
107 }
108 }
109}
110
111impl Edge {
112 fn merge(self, other: Self) -> Self {
115 Self {
116 inputs: self.inputs || other.inputs,
117 wait_for: self.wait_for.max(other.wait_for),
118 }
119 }
120}
121
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize)]
125#[serde(rename_all = "camelCase")]
126pub enum WaitFor {
127 #[default]
128 Started,
129 Ready,
130}
131
132#[derive(Default)]
134struct Dependencies {
135 ids: BTreeSet<String>,
136 edges: BTreeMap<String, Edge>,
137}
138
139impl Dependencies {
140 fn insert(&mut self, id: String, edge: Edge) {
141 let edge = if self.ids.insert(id.clone()) {
142 edge
143 } else {
144 self.edges.get(&id).copied().unwrap_or_default().merge(edge)
145 };
146 if edge == Edge::default() {
147 self.edges.remove(&id);
148 } else {
149 self.edges.insert(id, edge);
150 }
151 }
152}
153
154#[derive(Debug, Default, Serialize)]
155pub struct TaskGraph {
156 pub roots: BTreeSet<String>,
157 pub tasks: BTreeMap<String, Task>,
158 #[serde(skip)]
160 pub cycles: Vec<Vec<String>>,
161 #[serde(skip)]
164 pub requests: Vec<Request>,
165 #[serde(skip)]
166 pub options: BuildOptions,
167 #[serde(skip)]
171 pub excluded: Excluded,
172}
173
174#[derive(Clone, Debug, Default)]
176pub struct Excluded {
177 pub tasks: BTreeMap<String, Task>,
178 pub original_dependencies: BTreeMap<String, BTreeSet<String>>,
180}
181
182#[derive(Clone, Copy, Debug, Default)]
184pub struct BuildOptions {
185 pub exclude_task_dependencies: bool,
188 pub ignore_cycles: bool,
191}
192
193impl TaskGraph {
194 pub fn build(workspace: &Workspace, requests: &[Request]) -> Result<Self> {
195 Self::build_with(workspace, requests, BuildOptions::default())
196 }
197
198 pub fn build_with(
199 workspace: &Workspace,
200 requests: &[Request],
201 options: BuildOptions,
202 ) -> Result<Self> {
203 if requests.is_empty() {
204 bail!("no tasks selected");
205 }
206 let projects = ProjectGraph::build(workspace)?;
207 let mut builder = Builder {
208 workspace,
209 projects,
210 tasks: BTreeMap::new(),
211 visiting: Vec::new(),
212 visited: BTreeSet::new(),
213 ignore_cycles: options.ignore_cycles,
214 cycles: Vec::new(),
215 };
216 let mut roots = BTreeSet::new();
217 for request in requests {
218 let cli = request.args.clone();
219 let visit = Visit {
220 request: request.clone(),
221 overrides: Map::new(),
222 cli: &cli,
223 };
224 roots.insert(builder.visit(visit, true)?.with_context(|| {
225 format!(
226 "project {:?} has no target {:?}",
227 request.project, request.target
228 )
229 })?);
230 }
231 let mut graph = Self {
232 roots,
233 tasks: builder.tasks,
234 cycles: builder.cycles,
235 requests: requests.to_vec(),
236 options,
237 excluded: Excluded::default(),
238 };
239 if !graph.cycles.is_empty() {
240 graph.make_acyclic();
241 }
242 if options.exclude_task_dependencies {
243 let roots = graph.roots.clone();
244 let (kept, left): (BTreeMap<_, _>, BTreeMap<_, _>) = std::mem::take(&mut graph.tasks)
245 .into_iter()
246 .partition(|(id, _)| roots.contains(id));
247 graph.tasks = kept;
248 graph.excluded.tasks = left;
249 for task in graph.tasks.values_mut() {
250 graph
251 .excluded
252 .original_dependencies
253 .insert(task.id.clone(), task.dependencies.clone());
254 task.dependencies.retain(|id| roots.contains(id));
255 task.edges.retain(|id, _| roots.contains(id));
256 }
257 }
258 Ok(graph)
259 }
260
261 pub fn output_dependencies(&self, task: &Task, transitive: bool) -> BTreeSet<String> {
267 let mut selected = self
268 .excluded
269 .original_dependencies
270 .get(&task.id)
271 .unwrap_or(&task.dependencies)
272 .clone();
273 if transitive {
274 let mut pending = selected.clone();
275 while let Some(id) = pending.pop_first() {
276 let Some(dependency) = self.keyed_task(&id) else {
277 continue;
278 };
279 let dependencies = self
280 .excluded
281 .original_dependencies
282 .get(&id)
283 .unwrap_or(&dependency.dependencies);
284 for next in dependencies {
285 if selected.insert(next.clone()) {
286 pending.insert(next.clone());
287 }
288 }
289 }
290 }
291 selected
292 }
293
294 pub fn keyed_task(&self, id: &str) -> Option<&Task> {
297 self.tasks.get(id).or_else(|| self.excluded.tasks.get(id))
298 }
299
300 pub fn rebuild(&self, workspace: &Workspace, extra: &[Request]) -> Result<Self> {
303 let mut requests = self.requests.clone();
304 requests.extend_from_slice(extra);
305 Self::build_with(workspace, &requests, self.options)
306 }
307
308 fn make_acyclic(&mut self) {
311 fn visit(
312 graph: &mut BTreeMap<String, Task>,
313 id: &str,
314 visited: &mut BTreeSet<String>,
315 path: &mut Vec<String>,
316 ) {
317 if !visited.insert(id.to_owned()) {
318 return;
319 }
320 let dependencies: Vec<String> = graph[id].dependencies.iter().cloned().collect();
321 for dependency in dependencies {
322 if path.contains(&dependency) {
323 let task = graph.get_mut(id).unwrap();
324 task.dependencies.remove(&dependency);
325 task.edges.remove(&dependency);
326 } else {
327 path.push(dependency.clone());
328 visit(graph, &dependency, visited, path);
329 path.pop();
330 }
331 }
332 }
333 let ids: Vec<String> = self.tasks.keys().cloned().collect();
334 let mut visited = BTreeSet::new();
335 for id in ids {
336 let mut path = vec![id.clone()];
337 visit(&mut self.tasks, &id, &mut visited, &mut path);
338 }
339 }
340}
341
342struct Declared {
346 requests: Vec<Request>,
347 overrides: Map<String, Value>,
348 derived: bool,
349 edge: Edge,
350}
351
352struct Visit<'c> {
355 request: Request,
356 overrides: Map<String, Value>,
357 cli: &'c [String],
358}
359
360struct Builder<'a> {
361 workspace: &'a Workspace,
362 projects: ProjectGraph,
363 tasks: BTreeMap<String, Task>,
364 visiting: Vec<String>,
365 visited: BTreeSet<(String, Option<String>)>,
366 ignore_cycles: bool,
367 cycles: Vec<Vec<String>>,
368}
369
370impl Builder<'_> {
371 fn visit(&mut self, visit: Visit, required: bool) -> Result<Option<String>> {
372 let Visit {
373 request,
374 overrides,
375 cli,
376 } = visit;
377 let project = self
378 .workspace
379 .projects
380 .get(&request.project)
381 .with_context(|| format!("unknown project {:?}", request.project))?;
382 let Some(target) = project.targets.get(&request.target) else {
383 if required {
384 bail!(
385 "project {:?} has no target {:?}",
386 request.project,
387 request.target
388 );
389 }
390 return Ok(None);
391 };
392 let requested_configuration = request.requested_configuration.clone();
393 let configuration = match request.configuration {
394 Some(name) if target.configurations.contains_key(&name) => Some(name),
395 Some(name) if required => bail!(
396 "{}:{} has no configuration {name:?}",
397 request.project,
398 request.target
399 ),
400 _ => target
402 .default_configuration
403 .clone()
404 .filter(|name| !name.is_empty()),
405 };
406 let id = format!(
407 "{}:{}{}",
408 request.project,
409 request.target,
410 configuration
411 .as_ref()
412 .map(|c| format!(":{c}"))
413 .unwrap_or_default()
414 );
415 if let Some(position) = self.visiting.iter().position(|task| task == &id) {
416 let mut cycle = self.visiting[position..].to_vec();
417 cycle.push(id.clone());
418 if !self.ignore_cycles {
419 bail!("task dependency cycle: {}", cycle.join(" -> "));
420 }
421 self.cycles.push(cycle);
423 return Ok(Some(id));
424 }
425 if self
426 .visited
427 .contains(&(id.clone(), requested_configuration.clone()))
428 {
429 return Ok(Some(id));
430 }
431 let (args, definition) = match self.tasks.get(&id) {
434 Some(existing) => (existing.args.clone(), existing.definition.clone()),
435 None => {
436 let mut definition = target.clone();
437 if let Some(name) = &configuration {
438 let options = target
439 .configurations
440 .get(name)
441 .with_context(|| format!("{id}: default configuration does not exist"))?;
442 for (key, value) in options {
443 if key == "env" {
444 value.as_object().with_context(|| {
445 format!("{id}: configuration env must be an object")
446 })?;
447 }
448 definition.options.insert(key.clone(), value.clone());
449 }
450 }
451 definition.options.extend(overrides);
454 (request.args, definition)
455 }
456 };
457 if let Some(outputs) = &definition.outputs {
459 validate_outputs(outputs)?;
460 }
461 let task = Task {
462 id: id.clone(),
463 project: request.project,
464 target: request.target,
465 configuration,
466 args,
467 definition,
468 dependencies: BTreeSet::new(),
469 edges: BTreeMap::new(),
470 };
471 self.visiting.push(id.clone());
472 let mut dependencies = self
473 .tasks
474 .get(&id)
475 .map(|task| Dependencies {
476 ids: task.dependencies.clone(),
477 edges: task.edges.clone(),
478 })
479 .unwrap_or_default();
480 self.collect(
481 &task,
482 &task.project,
483 requested_configuration.as_deref(),
484 cli,
485 &mut dependencies,
486 &mut BTreeSet::new(),
487 )?;
488 self.visiting.pop();
489 self.visited.insert((id.clone(), requested_configuration));
490 self.tasks.insert(
491 id.clone(),
492 Task {
493 dependencies: dependencies.ids,
494 edges: dependencies.edges,
495 ..task
496 },
497 );
498 Ok(Some(id))
499 }
500
501 fn collect(
506 &mut self,
507 task: &Task,
508 derive_from: &str,
509 requested_configuration: Option<&str>,
510 cli: &[String],
511 dependencies: &mut Dependencies,
512 seen: &mut BTreeSet<String>,
513 ) -> Result<()> {
514 if !seen.insert(derive_from.to_owned()) {
515 return Ok(());
516 }
517 for dependency in task.definition.depends_on.as_deref().unwrap_or_default() {
518 let Declared {
519 requests,
520 overrides,
521 derived,
522 edge,
523 } = self
524 .dependencies(task, derive_from, requested_configuration, cli, dependency)
525 .with_context(|| format!("dependsOn of {}", task.id))?;
526 for request in requests {
527 let has_target = self.workspace.projects[&request.project]
528 .targets
529 .contains_key(&request.target);
530 if derived && !has_target {
531 let project = request.project;
532 self.collect(
533 task,
534 &project,
535 requested_configuration,
536 cli,
537 dependencies,
538 seen,
539 )?;
540 } else if request.project == task.project && request.target == task.target {
541 continue;
543 } else {
544 let visit = Visit {
545 request,
546 overrides: overrides.clone(),
547 cli,
548 };
549 if let Some(id) = self.visit(visit, false)? {
550 dependencies.insert(id, edge);
551 }
552 }
553 }
554 }
555 Ok(())
556 }
557
558 fn dependencies(
561 &self,
562 task: &Task,
563 derive_from: &str,
564 requested_configuration: Option<&str>,
565 cli: &[String],
566 value: &Value,
567 ) -> Result<Declared> {
568 let mut forward_options = false;
569 let mut edge = Edge::default();
570 let (target, projects, forward, derived) = match value {
571 Value::String(value) => {
572 if let Some(target) = value.strip_prefix('^') {
573 (
574 target.to_owned(),
575 self.project_dependencies(derive_from),
576 false,
577 true,
578 )
579 } else {
580 let (project, target) = self.target_reference(value, &task.project);
581 (target, vec![project], false, false)
582 }
583 }
584 Value::Object(object) => {
585 let target = object
588 .get("target")
589 .and_then(Value::as_str)
590 .context("dependency target must be a string")?;
591 let forward = object.get("params").and_then(Value::as_str) == Some("forward");
592 forward_options = object.get("options").and_then(Value::as_str) == Some("forward");
593 if let Some(inputs) = object.get("inputs") {
594 edge.inputs = inputs
595 .as_bool()
596 .context("dependency inputs must be boolean")?;
597 }
598 edge.wait_for = match object.get("waitFor").and_then(Value::as_str) {
599 None if !object.contains_key("waitFor") => WaitFor::Started,
600 Some("started") => WaitFor::Started,
601 Some("ready") => WaitFor::Ready,
602 _ => bail!("dependency waitFor must be started or ready"),
603 };
604 let dependencies = match object.get("dependencies") {
605 Some(value) => value
606 .as_bool()
607 .context("dependency dependencies must be boolean")?,
608 None => false,
609 };
610 let projects = if let Some(selectors) = object.get("projects") {
612 let selectors: Vec<String> = match selectors {
613 Value::String(selector)
616 if matches!(selector.as_str(), "self" | "dependencies")
617 && !self.workspace.projects.contains_key(selector) =>
618 {
619 bail!(
620 "projects: {selector:?} is no longer supported; leave projects \
621 out for the task's own project, or use dependencies: true"
622 )
623 }
624 Value::String(selector) => vec![selector.clone()],
625 value => serde_json::from_value(value.clone())
626 .context("dependency projects must be a string or string array")?,
627 };
628 qk_config::find_matching_projects(
629 &selectors,
630 &self.workspace.projects,
631 qk_config::Reader::Nx,
632 )
633 } else if dependencies {
634 self.project_dependencies(derive_from)
635 } else {
636 vec![task.project.clone()]
637 };
638 let derived = dependencies && !object.contains_key("projects");
639 (target.to_owned(), projects, forward, derived)
640 }
641 _ => bail!("dependency must be a string or object"),
642 };
643 let targets: Vec<String> = if qk_config::is_glob(&target) {
646 let matcher = qk_config::minimatch::Minimatch::new(&target, false);
647 self.workspace
648 .projects
649 .values()
650 .flat_map(|project| project.targets.keys())
651 .filter(|name| matcher.is_match(name))
652 .cloned()
653 .collect::<BTreeSet<_>>()
654 .into_iter()
655 .collect()
656 } else {
657 vec![target]
658 };
659 let mut overrides = Map::new();
663 if forward_options {
664 let source = &self.workspace.projects[&task.project].targets[&task.target];
665 overrides.extend(source.options.clone());
666 if let Some(options) = task
667 .configuration
668 .as_ref()
669 .and_then(|name| source.configurations.get(name))
670 {
671 overrides.extend(options.clone());
672 }
673 }
674 let args = if forward { cli.to_vec() } else { Vec::new() };
675 let requests = targets
676 .iter()
677 .flat_map(|target| {
678 projects.iter().map(|project| Request {
679 project: project.clone(),
680 target: target.clone(),
681 configuration: requested_configuration.map(str::to_owned),
682 requested_configuration: requested_configuration.map(str::to_owned),
683 args: args.clone(),
684 })
685 })
686 .collect();
687 Ok(Declared {
688 requests,
689 overrides,
690 derived,
691 edge,
692 })
693 }
694
695 fn target_reference(&self, text: &str, current: &str) -> (String, String) {
699 let parts = split_target(text, &self.workspace.projects, current);
700 match &parts[..] {
701 [target] => (current.to_owned(), target.clone()),
702 [project, rest @ ..] if self.workspace.projects.contains_key(project) => {
703 (project.clone(), rest.join(":"))
704 }
705 _ => (current.to_owned(), text.to_owned()),
706 }
707 }
708
709 fn project_dependencies(&self, project: &str) -> Vec<String> {
710 self.projects.dependencies[project]
712 .iter()
713 .map(|edge| edge.target.clone())
714 .collect::<BTreeSet<_>>()
715 .into_iter()
716 .collect()
717 }
718}
719
720fn split_by_colons(text: &str) -> Vec<String> {
723 let mut parts = vec![String::new()];
724 let mut chars = text.chars();
725 while let Some(c) = chars.next() {
726 match c {
727 ':' => parts.push(String::new()),
728 '"' => {
729 for c in chars.by_ref() {
730 if c == '"' {
731 break;
732 }
733 parts.last_mut().unwrap().push(c);
734 }
735 }
736 c => parts.last_mut().unwrap().push(c),
737 }
738 }
739 parts
740}
741
742fn readings(
746 parts: &[String],
747 projects: &BTreeMap<String, qk_config::Project>,
748 current: &str,
749) -> Vec<Vec<String>> {
750 let mut readings = Vec::new();
751 let read = |project: &str, rest: &[String], readings: &mut Vec<Vec<String>>| {
752 let targets = &projects[project].targets;
753 for end in 1..=rest.len() {
754 let target = rest[..end].join(":");
755 let Some(definition) = targets.get(&target) else {
756 continue;
757 };
758 if end == rest.len() {
759 readings.push(vec![project.to_owned(), target]);
760 } else {
761 let configuration = rest[end..].join(":");
762 if definition.configurations.contains_key(&configuration) {
763 readings.push(vec![project.to_owned(), target, configuration]);
764 }
765 }
766 }
767 };
768 if projects.contains_key(current) {
769 read(current, parts, &mut readings);
770 }
771 for end in 1..=parts.len() {
772 let project = parts[..end].join(":");
773 if !projects.contains_key(&project) {
774 continue;
775 }
776 if end == parts.len() {
777 readings.push(vec![project]);
778 } else {
779 read(&project, &parts[end..], &mut readings);
780 }
781 }
782 readings
783}
784
785fn best(readings: Vec<Vec<String>>, current: &str) -> Option<Vec<String>> {
788 let rank = |reading: &Vec<String>| {
789 let length = |index: usize| reading.get(index).map_or(0, String::len);
790 (reading[0] == current, length(0), length(1), length(2))
791 };
792 let mut best: Option<Vec<String>> = None;
793 for reading in readings {
794 if best.as_ref().is_none_or(|best| rank(&reading) > rank(best)) {
795 best = Some(reading);
796 }
797 }
798 best
799}
800
801fn split_target(
803 text: &str,
804 projects: &BTreeMap<String, qk_config::Project>,
805 current: &str,
806) -> Vec<String> {
807 let parts = split_by_colons(text);
808 if let Some(best) = best(readings(&parts, projects, current), current) {
809 return best;
810 }
811 let colon = match text.find(':') {
812 Some(0) => text[1..].find(':').map(|index| index + 1),
813 colon => colon,
814 };
815 if colon.is_none_or(|colon| colon == 0) {
816 return vec![text.to_owned()];
817 }
818 let mut project = parts[0].clone();
819 let mut rest = parts[1..].to_vec();
820 if project.is_empty() && !rest.is_empty() {
822 let mut absorbed = 1;
823 for count in (1..rest.len()).rev() {
824 if projects.contains_key(&format!(":{}", rest[..count].join(":"))) {
825 absorbed = count;
826 break;
827 }
828 }
829 project = format!(":{}", rest[..absorbed].join(":"));
830 rest.drain(..absorbed);
831 }
832 let Some(configuration) = rest.last().cloned() else {
833 return vec![project];
834 };
835 if let Some(head) = text.get(..text.len().saturating_sub(configuration.len() + 1))
837 && let Some(best) = best(readings(&split_by_colons(head), projects, current), current)
838 && best.len() == 2
839 {
840 return vec![best[0].clone(), best[1].clone(), configuration];
841 }
842 let mut result = vec![project.clone()];
843 let targets = projects.get(&project).map(|project| &project.targets);
844 for end in (1..=rest.len()).rev() {
845 let target = rest[..end].join(":");
846 if targets.is_some_and(|targets| targets.contains_key(&target)) {
847 result.push(target);
848 if end < rest.len() {
849 result.push(rest[end..].join(":"));
850 }
851 return result;
852 }
853 }
854 result.extend(rest);
855 result
856}
857
858pub fn validate_outputs(outputs: &[String]) -> Result<()> {
863 let missing_prefix: Vec<&String> = outputs
864 .iter()
865 .filter(|output| {
866 let mut chars = output.strip_prefix('!').unwrap_or(output).chars();
869 !(chars.next() == Some('{') && chars.next().is_some() && chars.any(|c| c == '}'))
870 })
871 .collect();
872 let workspace_globs: Vec<&String> = outputs
873 .iter()
874 .filter(|output| !missing_prefix.contains(output))
875 .filter(|output| {
876 ["!{workspaceRoot}", "{workspaceRoot}"]
877 .iter()
878 .any(|prefix| output.strip_prefix(prefix).is_some_and(has_no_directory))
879 })
880 .collect();
881 let mut message = String::new();
882 if !missing_prefix.is_empty() {
883 message.push_str(&format!(
884 "The following outputs are invalid: \n{}\n\nRun `nx repair` to fix this.",
885 missing_prefix
886 .iter()
887 .map(|output| format!(
888 " - {output}\n ** Reason: Outputs must start with either \"{{workspaceRoot}}/\" or \"{{projectRoot}}/\"."
889 ))
890 .collect::<Vec<_>>()
891 .join("\n")
892 ));
893 }
894 if !workspace_globs.is_empty() {
895 message.push_str(&format!(
896 "The following outputs are defined by a glob pattern from the workspace root: \n - {}\n\nThese can be slow, replace them with a more specific pattern.",
897 workspace_globs
898 .iter()
899 .map(|output| output.as_str())
900 .collect::<Vec<_>>()
901 .join("\n - ")
902 ));
903 }
904 if message.is_empty() {
905 Ok(())
906 } else {
907 bail!(message)
908 }
909}
910
911fn has_no_directory(glob: &str) -> bool {
916 let mut directory = Vec::new();
917 for segment in glob.split('/') {
918 let mut syntax = false;
919 let mut chars = segment.chars();
920 while let Some(c) = chars.next() {
921 match c {
922 '\\' => {
923 chars.next();
924 }
925 '*' | '?' | '[' | ']' | '{' | '}' | '(' | ')' | '!' => syntax = true,
926 _ => {}
927 }
928 }
929 if syntax {
930 return directory.join("/").is_empty();
931 }
932 directory.push(segment);
933 }
934 false
935}