sps_common/dependency/
resolver.rs

1// FILE: sps-core/src/dependency/resolver.rs
2
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use tracing::{debug, error, warn};
8
9use crate::dependency::{Dependency, DependencyTag};
10use crate::error::{Result, SpsError};
11use crate::formulary::Formulary;
12use crate::keg::KegRegistry;
13use crate::model::formula::Formula;
14
15// --- NodeInstallStrategy ---
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum NodeInstallStrategy {
18    BottlePreferred,
19    SourceOnly,
20    BottleOrFail,
21}
22
23// --- PerTargetInstallPreferences (local, minimal) ---
24#[derive(Debug, Clone, Default)]
25pub struct PerTargetInstallPreferences {
26    pub force_source_build_targets: HashSet<String>,
27    pub force_bottle_only_targets: HashSet<String>,
28}
29
30// --- ResolutionContext ---
31pub struct ResolutionContext<'a> {
32    pub formulary: &'a Formulary,
33    pub keg_registry: &'a KegRegistry,
34    pub sps_prefix: &'a Path,
35    pub include_optional: bool,
36    pub include_test: bool,
37    pub skip_recommended: bool,
38    pub initial_target_preferences: &'a PerTargetInstallPreferences,
39    pub build_all_from_source: bool,
40    pub cascade_source_preference_to_dependencies: bool,
41    /// Function pointer to check if a bottle is available for a formula.
42    pub has_bottle_for_current_platform: fn(&Formula) -> bool,
43}
44
45// --- ResolvedDependency ---
46#[derive(Debug, Clone)]
47pub struct ResolvedDependency {
48    pub formula: Arc<Formula>,
49    pub keg_path: Option<PathBuf>,
50    pub opt_path: Option<PathBuf>,
51    pub status: ResolutionStatus,
52    pub accumulated_tags: DependencyTag,
53    pub determined_install_strategy: NodeInstallStrategy,
54    pub failure_reason: Option<String>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ResolutionStatus {
59    Installed,
60    Missing,
61    Requested,
62    SkippedOptional,
63    NotFound,
64    Failed,
65}
66
67#[derive(Debug, Clone)]
68pub struct ResolvedGraph {
69    pub install_plan: Vec<ResolvedDependency>,
70    pub build_dependency_opt_paths: Vec<PathBuf>,
71    pub runtime_dependency_opt_paths: Vec<PathBuf>,
72    pub resolution_details: HashMap<String, ResolvedDependency>,
73}
74
75pub struct DependencyResolver<'a> {
76    context: ResolutionContext<'a>,
77    formula_cache: HashMap<String, Arc<Formula>>,
78    visiting: HashSet<String>,
79    resolution_details: HashMap<String, ResolvedDependency>,
80    // Store Arc<SpsError> instead of SpsError
81    errors: HashMap<String, Arc<SpsError>>,
82}
83
84impl<'a> DependencyResolver<'a> {
85    pub fn new(context: ResolutionContext<'a>) -> Self {
86        Self {
87            context,
88            formula_cache: HashMap::new(),
89            visiting: HashSet::new(),
90            resolution_details: HashMap::new(),
91            errors: HashMap::new(),
92        }
93    }
94
95    fn determine_node_install_strategy(
96        &self,
97        formula_name: &str,
98        formula_arc: &Arc<Formula>,
99        is_initial_target: bool,
100        requesting_parent_strategy: Option<NodeInstallStrategy>,
101    ) -> NodeInstallStrategy {
102        // 1. Per-target user preference
103        if is_initial_target {
104            if self
105                .context
106                .initial_target_preferences
107                .force_source_build_targets
108                .contains(formula_name)
109            {
110                debug!(
111                    "Strategy for initial target '{}': SourceOnly (user-forced for this target)",
112                    formula_name
113                );
114                return NodeInstallStrategy::SourceOnly;
115            }
116            if self
117                .context
118                .initial_target_preferences
119                .force_bottle_only_targets
120                .contains(formula_name)
121            {
122                debug!(
123                    "Strategy for initial target '{}': BottleOrFail (user-forced for this target)",
124                    formula_name
125                );
126                if !(self.context.has_bottle_for_current_platform)(formula_arc) {
127                    warn!(
128                        "User forced bottle for '{}', but no bottle is available.",
129                        formula_name
130                    );
131                }
132                return NodeInstallStrategy::BottleOrFail;
133            }
134        }
135        // 2. Global build-from-source
136        if self.context.build_all_from_source {
137            debug!(
138                "Strategy for '{}': SourceOnly (global --build-from-source active)",
139                formula_name
140            );
141            return NodeInstallStrategy::SourceOnly;
142        }
143        // 3. Cascade from parent
144        if self.context.cascade_source_preference_to_dependencies {
145            if let Some(NodeInstallStrategy::SourceOnly) = requesting_parent_strategy {
146                debug!(
147                    "Strategy for '{}': SourceOnly (cascaded from source-built parent)",
148                    formula_name
149                );
150                return NodeInstallStrategy::SourceOnly;
151            }
152        }
153        // 4. Cascade BottleOrFail from parent
154        if let Some(NodeInstallStrategy::BottleOrFail) = requesting_parent_strategy {
155            debug!(
156                "Strategy for '{}' (dependency of BottleOrFail parent): BottleOrFail (cascaded)",
157                formula_name
158            );
159            return NodeInstallStrategy::BottleOrFail;
160        }
161        // 5. Default: bottle if available, else source
162        let determined_strategy = if (self.context.has_bottle_for_current_platform)(formula_arc) {
163            NodeInstallStrategy::BottlePreferred
164        } else {
165            NodeInstallStrategy::SourceOnly
166        };
167
168        debug!(
169            "Strategy for '{}': {:?} (InitialTarget: {}, ParentStrategy: {:?}, BottleAvailable: {})",
170            formula_name,
171            determined_strategy,
172            is_initial_target,
173            requesting_parent_strategy,
174            (self.context.has_bottle_for_current_platform)(formula_arc)
175        );
176        determined_strategy
177    }
178
179    pub fn resolve_targets(&mut self, targets: &[String]) -> Result<ResolvedGraph> {
180        debug!("Starting dependency resolution for targets: {:?}", targets);
181        self.visiting.clear();
182        self.resolution_details.clear();
183        self.errors.clear();
184
185        for target_name in targets {
186            if let Err(e) = self.resolve_recursive(target_name, DependencyTag::RUNTIME, true, None)
187            {
188                self.errors.insert(target_name.clone(), Arc::new(e));
189                warn!(
190                    "Resolution failed for target '{}', but continuing for others.",
191                    target_name
192                );
193            }
194        }
195
196        debug!(
197            "Raw resolved map after initial pass: {:?}",
198            self.resolution_details
199                .iter()
200                .map(|(k, v)| (k.clone(), v.status, v.accumulated_tags))
201                .collect::<Vec<_>>()
202        );
203
204        let sorted_list = match self.topological_sort() {
205            Ok(list) => list,
206            Err(e @ SpsError::DependencyError(_)) => {
207                error!("Topological sort failed due to dependency cycle: {}", e);
208                return Err(e);
209            }
210            Err(e) => {
211                error!("Topological sort failed: {}", e);
212                return Err(e);
213            }
214        };
215
216        let install_plan: Vec<ResolvedDependency> = sorted_list
217            .into_iter()
218            .filter(|dep| {
219                matches!(
220                    dep.status,
221                    ResolutionStatus::Missing | ResolutionStatus::Requested
222                )
223            })
224            .collect();
225
226        let mut build_paths = Vec::new();
227        let mut runtime_paths = Vec::new();
228        let mut seen_build_paths = HashSet::new();
229        let mut seen_runtime_paths = HashSet::new();
230
231        for dep in self.resolution_details.values() {
232            if matches!(
233                dep.status,
234                ResolutionStatus::Installed
235                    | ResolutionStatus::Requested
236                    | ResolutionStatus::Missing
237            ) {
238                if let Some(opt_path) = &dep.opt_path {
239                    if dep.accumulated_tags.contains(DependencyTag::BUILD)
240                        && seen_build_paths.insert(opt_path.clone())
241                    {
242                        debug!("Adding build dep path: {}", opt_path.display());
243                        build_paths.push(opt_path.clone());
244                    }
245                    if dep.accumulated_tags.intersects(
246                        DependencyTag::RUNTIME
247                            | DependencyTag::RECOMMENDED
248                            | DependencyTag::OPTIONAL,
249                    ) && seen_runtime_paths.insert(opt_path.clone())
250                    {
251                        debug!("Adding runtime dep path: {}", opt_path.display());
252                        runtime_paths.push(opt_path.clone());
253                    }
254                } else if dep.status != ResolutionStatus::NotFound
255                    && dep.status != ResolutionStatus::Failed
256                {
257                    debug!(
258                        "Warning: No opt_path found for resolved dependency {} ({:?})",
259                        dep.formula.name(),
260                        dep.status
261                    );
262                }
263            }
264        }
265
266        if !self.errors.is_empty() {
267            warn!(
268                "Resolution encountered errors for specific targets: {:?}",
269                self.errors
270                    .iter()
271                    .map(|(k, v)| (k, v.to_string()))
272                    .collect::<HashMap<_, _>>()
273            );
274        }
275
276        debug!(
277            "Final installation plan (needs install/build): {:?}",
278            install_plan
279                .iter()
280                .map(|d| (d.formula.name(), d.status))
281                .collect::<Vec<_>>()
282        );
283        debug!(
284            "Collected build dependency paths: {:?}",
285            build_paths.iter().map(|p| p.display()).collect::<Vec<_>>()
286        );
287        debug!(
288            "Collected runtime dependency paths: {:?}",
289            runtime_paths
290                .iter()
291                .map(|p| p.display())
292                .collect::<Vec<_>>()
293        );
294
295        Ok(ResolvedGraph {
296            install_plan,
297            build_dependency_opt_paths: build_paths,
298            runtime_dependency_opt_paths: runtime_paths,
299            resolution_details: self.resolution_details.clone(),
300        })
301    }
302
303    /// Walk a dependency node, collecting status and propagating errors
304    fn resolve_recursive(
305        &mut self,
306        name: &str,
307        tags_from_parent_edge: DependencyTag,
308        is_initial_target: bool,
309        requesting_parent_strategy: Option<NodeInstallStrategy>,
310    ) -> Result<()> {
311        debug!(
312            "Resolving: {} (requested as {:?}, is_target: {})",
313            name, tags_from_parent_edge, is_initial_target
314        );
315
316        // -------- cycle guard -------------------------------------------------------------
317        if self.visiting.contains(name) {
318            error!("Dependency cycle detected involving: {}", name);
319            return Err(SpsError::DependencyError(format!(
320                "Dependency cycle detected involving '{name}'"
321            )));
322        }
323
324        // -------- if we have a previous entry, maybe promote status / tags -----------------
325        if let Some(existing) = self.resolution_details.get_mut(name) {
326            let original_status = existing.status;
327            let original_tags = existing.accumulated_tags;
328
329            // status promotion rules -------------------------------------------------------
330            let mut new_status = original_status;
331            if is_initial_target && new_status == ResolutionStatus::Missing {
332                new_status = ResolutionStatus::Requested;
333            }
334            if new_status == ResolutionStatus::SkippedOptional
335                && (tags_from_parent_edge.contains(DependencyTag::RUNTIME)
336                    || tags_from_parent_edge.contains(DependencyTag::BUILD)
337                    || (tags_from_parent_edge.contains(DependencyTag::RECOMMENDED)
338                        && !self.context.skip_recommended)
339                    || (is_initial_target && self.context.include_optional))
340            {
341                new_status = if existing.keg_path.is_some() {
342                    ResolutionStatus::Installed
343                } else if is_initial_target {
344                    ResolutionStatus::Requested
345                } else {
346                    ResolutionStatus::Missing
347                };
348            }
349
350            // apply any changes ------------------------------------------------------------
351            let mut needs_revisit = false;
352            if new_status != original_status {
353                debug!(
354                    "Updating status for '{name}' from {:?} to {:?}",
355                    original_status, new_status
356                );
357                existing.status = new_status;
358                needs_revisit = true;
359            }
360
361            let combined_tags = original_tags | tags_from_parent_edge;
362            if combined_tags != original_tags {
363                debug!(
364                    "Updating tags for '{name}' from {:?} to {:?}",
365                    original_tags, combined_tags
366                );
367                existing.accumulated_tags = combined_tags;
368                needs_revisit = true;
369            }
370
371            // nothing else to do
372            if !needs_revisit {
373                debug!("'{}' already resolved with compatible status/tags.", name);
374                return Ok(());
375            }
376
377            debug!(
378                "Re-evaluating dependencies for '{}' due to status/tag update",
379                name
380            );
381        }
382        // -------- first time we see this node ---------------------------------------------
383        else {
384            self.visiting.insert(name.to_string());
385
386            // load / cache the formula -----------------------------------------------------
387            let formula_arc = match self.formula_cache.get(name) {
388                Some(f) => f.clone(),
389                None => {
390                    debug!("Loading formula definition for '{}'", name);
391                    match self.context.formulary.load_formula(name) {
392                        Ok(f) => {
393                            let arc = Arc::new(f);
394                            self.formula_cache.insert(name.to_string(), arc.clone());
395                            arc
396                        }
397                        Err(e) => {
398                            error!("Failed to load formula definition for '{}': {}", name, e);
399                            let msg = e.to_string();
400                            self.resolution_details.insert(
401                                name.to_string(),
402                                ResolvedDependency {
403                                    formula: Arc::new(Formula::placeholder(name)),
404                                    keg_path: None,
405                                    opt_path: None,
406                                    status: ResolutionStatus::NotFound,
407                                    accumulated_tags: tags_from_parent_edge,
408                                    determined_install_strategy:
409                                        NodeInstallStrategy::BottlePreferred,
410                                    failure_reason: Some(msg.clone()),
411                                },
412                            );
413                            self.visiting.remove(name);
414                            self.errors
415                                .insert(name.to_string(), Arc::new(SpsError::NotFound(msg)));
416                            return Ok(()); // treat “not found” as a soft failure
417                        }
418                    }
419                }
420            };
421
422            let current_node_strategy = self.determine_node_install_strategy(
423                name,
424                &formula_arc,
425                is_initial_target,
426                requesting_parent_strategy,
427            );
428
429            let (status, keg_path) = match current_node_strategy {
430                NodeInstallStrategy::SourceOnly => (
431                    if is_initial_target {
432                        ResolutionStatus::Requested
433                    } else {
434                        ResolutionStatus::Missing
435                    },
436                    None,
437                ),
438                NodeInstallStrategy::BottlePreferred | NodeInstallStrategy::BottleOrFail => {
439                    if let Some(keg) = self.context.keg_registry.get_installed_keg(name)? {
440                        (ResolutionStatus::Installed, Some(keg.path))
441                    } else {
442                        (
443                            if is_initial_target {
444                                ResolutionStatus::Requested
445                            } else {
446                                ResolutionStatus::Missing
447                            },
448                            None,
449                        )
450                    }
451                }
452            };
453
454            debug!(
455                "Initial status for '{}': {:?}, keg: {:?}, opt: {}",
456                name,
457                status,
458                keg_path,
459                self.context.keg_registry.get_opt_path(name).display()
460            );
461
462            self.resolution_details.insert(
463                name.to_string(),
464                ResolvedDependency {
465                    formula: formula_arc.clone(),
466                    keg_path,
467                    opt_path: Some(self.context.keg_registry.get_opt_path(name)),
468                    status,
469                    accumulated_tags: tags_from_parent_edge,
470                    determined_install_strategy: current_node_strategy,
471                    failure_reason: None,
472                },
473            );
474        }
475
476        // --------------------------------------------------------------------- recurse ----
477        let dep_snapshot = self
478            .resolution_details
479            .get(name)
480            .expect("just inserted")
481            .clone();
482
483        // if this node is already irrecoverably broken, stop here
484        if matches!(
485            dep_snapshot.status,
486            ResolutionStatus::Failed | ResolutionStatus::NotFound
487        ) {
488            self.visiting.remove(name);
489            return Ok(());
490        }
491
492        // iterate its declared dependencies -----------------------------------------------
493        for dep in dep_snapshot.formula.dependencies()? {
494            let dep_name = &dep.name;
495            let dep_tags = dep.tags;
496            let parent_name = dep_snapshot.formula.name();
497            let parent_strategy = dep_snapshot.determined_install_strategy;
498
499            debug!(
500                "RESOLVER: Evaluating edge: parent='{}' ({:?}), child='{}' ({:?})",
501                parent_name, parent_strategy, dep_name, dep_tags
502            );
503
504            // optional / test filtering
505            if !self.should_consider_dependency(&dep) {
506                if !self.resolution_details.contains_key(dep_name.as_str()) {
507                    debug!("RESOLVER: Child '{}' of '{}' globally SKIPPED (e.g. optional/test not included). Tags: {:?}", dep_name, parent_name, dep_tags);
508                    // ...existing code for SkippedOptional...
509                }
510                continue;
511            }
512
513            // Specific edge processing based on parent strategy
514            let should_process = self.context.should_process_dependency_edge(
515                &dep_snapshot.formula,
516                dep_tags,
517                parent_strategy,
518            );
519
520            if !should_process {
521                debug!(
522                    "RESOLVER: Edge from '{}' (Strategy: {:?}) to child '{}' (Tags: {:?}) was SKIPPED by should_process_dependency_edge.",
523                    parent_name, parent_strategy, dep_name, dep_tags
524                );
525                // ...existing code for skipping...
526                continue;
527            }
528
529            debug!(
530                "RESOLVER: Edge from '{}' (Strategy: {:?}) to child '{}' (Tags: {:?}) WILL BE PROCESSED. Recursing.",
531                parent_name, parent_strategy, dep_name, dep_tags
532            );
533
534            // --- real recursion -----------------------------------------------------------
535            if let Err(_e) =
536                self.resolve_recursive(dep_name, dep_tags, false, Some(parent_strategy))
537            {
538                // ...existing code...
539            }
540        }
541
542        self.visiting.remove(name);
543        debug!("Finished resolving '{}'", name);
544        Ok(())
545    }
546
547    fn topological_sort(&self) -> Result<Vec<ResolvedDependency>> {
548        let mut in_degree: HashMap<String, usize> = HashMap::new();
549        let mut adj: HashMap<String, HashSet<String>> = HashMap::new();
550        let mut sorted_list = Vec::new();
551        let mut queue = VecDeque::new();
552
553        let relevant_nodes_map: HashMap<String, &ResolvedDependency> = self
554            .resolution_details
555            .iter()
556            .filter(|(_, dep)| {
557                !matches!(
558                    dep.status,
559                    ResolutionStatus::NotFound | ResolutionStatus::Failed
560                )
561            })
562            .map(|(k, v)| (k.clone(), v))
563            .collect();
564
565        for (parent_name, parent_rd) in &relevant_nodes_map {
566            adj.entry(parent_name.clone()).or_default();
567            in_degree.entry(parent_name.clone()).or_default();
568
569            let parent_strategy = parent_rd.determined_install_strategy;
570            if let Ok(dependencies) = parent_rd.formula.dependencies() {
571                for child_edge in dependencies {
572                    let child_name = &child_edge.name;
573                    if relevant_nodes_map.contains_key(child_name)
574                        && self.context.should_process_dependency_edge(
575                            &parent_rd.formula,
576                            child_edge.tags,
577                            parent_strategy,
578                        )
579                        && adj
580                            .entry(parent_name.clone())
581                            .or_default()
582                            .insert(child_name.clone())
583                    {
584                        *in_degree.entry(child_name.clone()).or_default() += 1;
585                    }
586                }
587            }
588        }
589
590        for name in relevant_nodes_map.keys() {
591            if *in_degree.get(name).unwrap_or(&1) == 0 {
592                queue.push_back(name.clone());
593            }
594        }
595
596        while let Some(u_name) = queue.pop_front() {
597            if let Some(resolved_dep) = relevant_nodes_map.get(&u_name) {
598                sorted_list.push((**resolved_dep).clone());
599            }
600            if let Some(neighbors) = adj.get(&u_name) {
601                for v_name in neighbors {
602                    if relevant_nodes_map.contains_key(v_name) {
603                        if let Some(degree) = in_degree.get_mut(v_name) {
604                            *degree = degree.saturating_sub(1);
605                            if *degree == 0 {
606                                queue.push_back(v_name.clone());
607                            }
608                        }
609                    }
610                }
611            }
612        }
613
614        let install_plan: Vec<ResolvedDependency> = sorted_list
615            .into_iter()
616            .filter(|dep| {
617                matches!(
618                    dep.status,
619                    ResolutionStatus::Missing | ResolutionStatus::Requested
620                )
621            })
622            .collect();
623
624        let expected_installable_count = relevant_nodes_map
625            .values()
626            .filter(|dep| {
627                matches!(
628                    dep.status,
629                    ResolutionStatus::Missing | ResolutionStatus::Requested
630                )
631            })
632            .count();
633
634        if install_plan.len() != expected_installable_count && expected_installable_count > 0 {
635            error!(
636                "Cycle detected! Sorted count ({}) != Relevant node count ({}).",
637                install_plan.len(),
638                expected_installable_count
639            );
640            return Err(SpsError::DependencyError(
641                "Circular dependency detected".to_string(),
642            ));
643        }
644        Ok(install_plan)
645    }
646
647    fn should_consider_dependency(&self, dep: &Dependency) -> bool {
648        let tags = dep.tags;
649        if tags.contains(DependencyTag::TEST) && !self.context.include_test {
650            return false;
651        }
652        if tags.contains(DependencyTag::OPTIONAL) && !self.context.include_optional {
653            return false;
654        }
655        if tags.contains(DependencyTag::RECOMMENDED) && self.context.skip_recommended {
656            return false;
657        }
658        true
659    }
660}
661
662impl Formula {
663    fn placeholder(name: &str) -> Self {
664        Self {
665            name: name.to_string(),
666            stable_version_str: "0.0.0".to_string(),
667            version_semver: semver::Version::new(0, 0, 0),
668            revision: 0,
669            desc: Some("Placeholder for unresolved formula".to_string()),
670            homepage: None,
671            url: String::new(),
672            sha256: String::new(),
673            mirrors: Vec::new(),
674            bottle: Default::default(),
675            dependencies: Vec::new(),
676            requirements: Vec::new(),
677            resources: Vec::new(),
678            install_keg_path: None,
679        }
680    }
681}
682
683// --- ResolutionContext methods ---
684impl<'a> ResolutionContext<'a> {
685    pub fn should_process_dependency_edge(
686        &self,
687        parent_formula_for_logging: &Arc<Formula>,
688        edge_tags: DependencyTag,
689        parent_node_determined_strategy: NodeInstallStrategy,
690    ) -> bool {
691        debug!(
692            "should_process_dependency_edge: Parent='{}', EdgeTags={:?}, ParentStrategy={:?}",
693            parent_formula_for_logging.name(),
694            edge_tags,
695            parent_node_determined_strategy
696        );
697
698        if edge_tags.contains(DependencyTag::TEST) && !self.include_test {
699            debug!(
700                "Edge with tags {:?} skipped: TEST dependencies excluded by context.",
701                edge_tags
702            );
703            return false;
704        }
705        if edge_tags.contains(DependencyTag::OPTIONAL) && !self.include_optional {
706            debug!(
707                "Edge with tags {:?} skipped: OPTIONAL dependencies excluded by context.",
708                edge_tags
709            );
710            return false;
711        }
712        if edge_tags.contains(DependencyTag::RECOMMENDED) && self.skip_recommended {
713            debug!(
714                "Edge with tags {:?} skipped: RECOMMENDED dependencies excluded by context.",
715                edge_tags
716            );
717            return false;
718        }
719        match parent_node_determined_strategy {
720            NodeInstallStrategy::BottlePreferred | NodeInstallStrategy::BottleOrFail => {
721                let is_purely_build_dependency = edge_tags.contains(DependencyTag::BUILD)
722                    && !edge_tags.intersects(
723                        DependencyTag::RUNTIME
724                            | DependencyTag::RECOMMENDED
725                            | DependencyTag::OPTIONAL,
726                    );
727                debug!(
728                    "Parent is bottled. For edge with tags {:?}, is_purely_build_dependency: {}",
729                    edge_tags, is_purely_build_dependency
730                );
731                if is_purely_build_dependency {
732                    debug!("Edge with tags {:?} SKIPPED: Pure BUILD dependency of a bottle-installed parent '{}'.", edge_tags, parent_formula_for_logging.name());
733                    return false;
734                }
735            }
736            NodeInstallStrategy::SourceOnly => {
737                debug!("Parent is SourceOnly. Edge with tags {:?} will be processed (unless globally skipped).", edge_tags);
738            }
739        }
740        debug!(
741            "Edge with tags {:?} WILL BE PROCESSED for parent '{}' with strategy {:?}.",
742            edge_tags,
743            parent_formula_for_logging.name(),
744            parent_node_determined_strategy
745        );
746        true
747    }
748
749    pub fn should_consider_edge_globally(&self, edge_tags: DependencyTag) -> bool {
750        if edge_tags.contains(DependencyTag::TEST) && !self.include_test {
751            return false;
752        }
753        if edge_tags.contains(DependencyTag::OPTIONAL) && !self.include_optional {
754            return false;
755        }
756        if edge_tags.contains(DependencyTag::RECOMMENDED) && self.skip_recommended {
757            return false;
758        }
759        true
760    }
761}