Skip to main content

mib_rs/mib/
trace.rs

1//! Structured, resolver-domain-specific symbol resolution explanations.
2
3use std::fmt;
4
5use crate::mib::{ImportResolution, Mib, ModuleId, Oid, Symbol, UnresolvedRef};
6use crate::types::{ResolutionDomain, ResolverStrictness};
7
8/// The kind of a candidate definition in a resolution trace.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum ResolutionCandidateKind {
11    /// An `OBJECT-TYPE` definition.
12    Object,
13    /// A `NOTIFICATION-TYPE` or `TRAP-TYPE` definition.
14    Notification,
15    /// An `OBJECT-GROUP` or `NOTIFICATION-GROUP` definition.
16    Group,
17    /// A `MODULE-COMPLIANCE` definition.
18    Compliance,
19    /// An `AGENT-CAPABILITIES` definition.
20    Capability,
21    /// A type assignment or `TEXTUAL-CONVENTION` definition.
22    Type,
23    /// An OID assignment without a more specific semantic definition.
24    Node,
25}
26
27impl From<Symbol> for ResolutionCandidateKind {
28    fn from(symbol: Symbol) -> Self {
29        match symbol {
30            Symbol::Object(_) => Self::Object,
31            Symbol::Notification(_) => Self::Notification,
32            Symbol::Group(_) => Self::Group,
33            Symbol::Compliance(_) => Self::Compliance,
34            Symbol::Capability(_) => Self::Capability,
35            Symbol::Type(_) => Self::Type,
36            Symbol::Node(_) => Self::Node,
37        }
38    }
39}
40
41impl fmt::Display for ResolutionCandidateKind {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(match self {
44            Self::Object => "object",
45            Self::Notification => "notification",
46            Self::Group => "group",
47            Self::Compliance => "compliance",
48            Self::Capability => "capability",
49            Self::Type => "type",
50            Self::Node => "node",
51        })
52    }
53}
54
55/// One loaded definition with the traced name.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ResolutionCandidate {
58    /// Identifies the exact loaded module version that defines the symbol.
59    pub module: ModuleId,
60    /// Names the module that defines the symbol.
61    pub module_name: String,
62    /// Identifies the module source when the loader assigned a label.
63    pub source_label: Option<String>,
64    /// Contains the module's `LAST-UPDATED` value, or an empty string when absent.
65    pub last_updated: String,
66    /// Classifies the definition.
67    pub kind: ResolutionCandidateKind,
68    /// Identifies the definition in the resolved MIB arenas.
69    pub symbol: Symbol,
70    /// Contains the definition's numeric OID when it has a resolved OID node.
71    pub oid: Option<Oid>,
72    /// Whether this definition kind can satisfy the selected domain.
73    pub applicable: bool,
74}
75
76/// Exact loaded module version used as a resolution scope.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ResolutionScope {
79    /// Identifies the exact loaded module version used as the scope.
80    pub module: ModuleId,
81    /// Names the scoped module.
82    pub module_name: String,
83    /// Identifies the scoped module's source when the loader assigned a label.
84    pub source_label: Option<String>,
85    /// Contains the module's `LAST-UPDATED` value, or an empty string when absent.
86    pub last_updated: String,
87}
88
89/// Resolver fallback tiers applicable to this domain, strictness, and name.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct ResolutionFallbackPolicy {
92    /// Indicates whether this name has an intrinsic foundation-module rule.
93    pub intrinsic: bool,
94    /// Indicates whether the domain and strictness enable foundation-module fallback.
95    pub constrained: bool,
96    /// Indicates whether the domain and strictness enable global fallback.
97    pub global: bool,
98}
99
100/// Strategy that selected the final definition.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ResolutionStrategy {
103    /// Selected a definition in the scoped module.
104    Local,
105    /// Followed an import directly to its declared source module.
106    DirectImport,
107    /// Followed an import re-exported through one or more modules.
108    ForwardedImport,
109    /// Followed the resolved portion of a partially resolved import clause.
110    PartialImport,
111    /// Followed a compatibility alias for the imported module name.
112    AliasImport,
113    /// Selected a definition from an intrinsic foundation module.
114    IntrinsicFallback,
115    /// Selected a definition from a strictness-dependent foundation module.
116    ConstrainedFallback,
117    /// Selected a definition through global fallback.
118    GlobalFallback,
119    /// Selected the only applicable candidate during an unscoped lookup.
120    UniqueCandidate,
121}
122
123impl fmt::Display for ResolutionStrategy {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter.write_str(match self {
126            Self::Local => "local definition",
127            Self::DirectImport => "direct import",
128            Self::ForwardedImport => "forwarded import",
129            Self::PartialImport => "partial import",
130            Self::AliasImport => "import alias",
131            Self::IntrinsicFallback => "intrinsic fallback",
132            Self::ConstrainedFallback => "constrained fallback",
133            Self::GlobalFallback => "global fallback",
134            Self::UniqueCandidate => "unique unscoped candidate",
135        })
136    }
137}
138
139/// Final classification of a traced lookup.
140///
141/// [`ResolutionTrace::target`] is present exactly when the outcome is
142/// [`Resolved`](Self::Resolved). An unscoped lookup is ambiguous when multiple
143/// applicable candidates exist. Scoped lookups report an unresolved lookup as
144/// [`Missing`](Self::Missing), even when other modules define the same name.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ResolutionOutcome {
147    /// The lookup selected one target.
148    Resolved,
149    /// An unscoped lookup found multiple applicable candidates.
150    Ambiguous,
151    /// The applicable resolver rules did not select a target.
152    Missing,
153}
154
155/// Definition selected by a traced lookup and the strategy that selected it.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct ResolutionTarget {
158    /// Describes the selected loaded definition.
159    pub candidate: ResolutionCandidate,
160    /// Identifies the resolver strategy that selected the definition.
161    pub strategy: ResolutionStrategy,
162}
163
164/// Structured explanation of one domain-specific name lookup.
165#[derive(Debug, Clone)]
166pub struct ResolutionTrace {
167    /// Preserves the original query exactly as supplied by the caller.
168    pub query: String,
169    /// Contains the unqualified symbol name extracted from `query`.
170    pub symbol: String,
171    /// Identifies the resolver domain whose definition rules were applied.
172    pub domain: ResolutionDomain,
173    /// Contains the exact module scope, or `None` for an unscoped lookup.
174    pub scope: Option<ResolutionScope>,
175    /// Records the resolver strictness active on the MIB.
176    pub strictness: ResolverStrictness,
177    /// Describes the fallback tiers enabled for this domain, strictness, and name.
178    pub fallbacks: ResolutionFallbackPolicy,
179    /// Every cross-kind definition with this name, deterministically ordered.
180    pub candidates: Vec<ResolutionCandidate>,
181    /// Exact pre-collapse import provenance for the scope, when it imports the name.
182    pub import: Option<ImportResolution>,
183    /// Classifies the final lookup result.
184    pub outcome: ResolutionOutcome,
185    /// Contains the selected definition exactly when `outcome` is resolved.
186    pub target: Option<ResolutionTarget>,
187    /// Lists unresolved references with the traced symbol name across loaded modules.
188    ///
189    /// The list is diagnostic provenance and does not determine `outcome`.
190    pub unresolved: Vec<UnresolvedRef>,
191}
192
193/// Failure to parse a trace query or select an exact module scope.
194#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
195pub enum ResolutionTraceError {
196    /// The query contains no characters.
197    #[error("symbol query is empty")]
198    EmptyQuery,
199    /// The qualified query does not have exactly one nonempty `module::symbol` pair.
200    #[error("invalid qualified symbol query: {0}")]
201    InvalidQualifiedQuery(String),
202    /// The qualified query and explicit scope name different modules.
203    #[error("qualified query scope {query_scope:?} conflicts with --module {explicit_scope:?}")]
204    ConflictingScope {
205        /// Names the module in the qualified query.
206        query_scope: String,
207        /// Names the separately supplied module scope.
208        explicit_scope: String,
209    },
210    /// No loaded module has the requested scope name.
211    #[error("module scope not found: {0}")]
212    ModuleNotFound(String),
213    /// Multiple loaded module versions have the requested scope name.
214    #[error("module scope {module:?} is ambiguous across loaded sources: {candidates:?}")]
215    AmbiguousModuleScope {
216        /// Names the requested module.
217        module: String,
218        /// Lists every exact loaded module version with the requested name.
219        candidates: Vec<ResolutionScope>,
220    },
221}
222
223impl Mib {
224    /// Explains a symbol lookup using the exact rules for `domain`.
225    ///
226    /// Use `module::symbol` in `query` to establish module scope, or pass
227    /// `module_scope` with an unqualified query. If both forms provide the same
228    /// module name, the lookup uses that scope. If they disagree, the method
229    /// returns [`ResolutionTraceError::ConflictingScope`]. A module name must
230    /// identify exactly one loaded version; duplicate versions return
231    /// [`ResolutionTraceError::AmbiguousModuleScope`] with every candidate.
232    ///
233    /// A scoped lookup follows the same local, import, and fallback rules as
234    /// semantic resolution for `domain`. An unscoped lookup does not infer
235    /// imports or fallbacks: it resolves only when exactly one loaded definition
236    /// has the requested name and an applicable kind. Multiple applicable
237    /// definitions produce [`ResolutionOutcome::Ambiguous`], and no applicable
238    /// definition produces [`ResolutionOutcome::Missing`].
239    ///
240    /// On success, [`ResolutionTrace::target`] is present exactly when
241    /// [`ResolutionTrace::outcome`] is [`ResolutionOutcome::Resolved`].
242    /// Candidate and unresolved-reference lists use deterministic ordering.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use mib_rs::load::Loader;
248    /// use mib_rs::mib::{ResolutionOutcome, ResolutionStrategy};
249    /// use mib_rs::source::memory;
250    /// use mib_rs::types::ResolutionDomain;
251    ///
252    /// let source = memory(
253    ///     "TRACE-EXAMPLE-MIB",
254    ///     b"TRACE-EXAMPLE-MIB DEFINITIONS ::= BEGIN\n\
255    ///       traceRoot OBJECT IDENTIFIER ::= { iso 424300 }\n\
256    ///       END\n",
257    /// );
258    /// let mib = Loader::new()
259    ///     .source(source)
260    ///     .modules(["TRACE-EXAMPLE-MIB"])
261    ///     .load()?;
262    /// let trace = mib.trace_symbol(
263    ///     "TRACE-EXAMPLE-MIB::traceRoot",
264    ///     None,
265    ///     ResolutionDomain::Oid,
266    /// )?;
267    ///
268    /// assert_eq!(trace.outcome, ResolutionOutcome::Resolved);
269    /// assert_eq!(trace.target.unwrap().strategy, ResolutionStrategy::Local);
270    /// # Ok::<(), Box<dyn std::error::Error>>(())
271    /// ```
272    pub fn trace_symbol(
273        &self,
274        query: &str,
275        module_scope: Option<&str>,
276        domain: ResolutionDomain,
277    ) -> Result<ResolutionTrace, ResolutionTraceError> {
278        let (qualified_scope, symbol_name) = parse_query(query)?;
279        let scope_name = match (qualified_scope, module_scope) {
280            (Some(query_scope), Some(explicit_scope)) if query_scope != explicit_scope => {
281                return Err(ResolutionTraceError::ConflictingScope {
282                    query_scope: query_scope.to_owned(),
283                    explicit_scope: explicit_scope.to_owned(),
284                });
285            }
286            (Some(query_scope), _) => Some(query_scope),
287            (None, Some(explicit_scope)) => Some(explicit_scope),
288            (None, None) => None,
289        };
290        let scope = scope_name
291            .map(|name| self.unique_resolution_scope(name))
292            .transpose()?;
293        let candidates = self.resolution_candidates(symbol_name, domain);
294        let fallback_domain = super::resolver::rules::fallback_domain(domain, symbol_name);
295        let fallbacks = ResolutionFallbackPolicy {
296            intrinsic: super::resolver::rules::intrinsic_foundation_module(
297                fallback_domain,
298                symbol_name,
299            )
300            .is_some(),
301            constrained: !super::resolver::rules::constrained_foundation_modules(
302                fallback_domain,
303                self.resolver_strictness(),
304            )
305            .is_empty(),
306            global: super::resolver::rules::allows_global_fallback(
307                fallback_domain,
308                self.resolver_strictness(),
309            ),
310        };
311
312        let (target, import) = match &scope {
313            Some(scope) => self.resolve_trace_in_scope(scope.module, symbol_name, domain),
314            None => {
315                let applicable = candidates
316                    .iter()
317                    .filter(|candidate| candidate.applicable)
318                    .collect::<Vec<_>>();
319                if applicable.len() == 1 {
320                    (
321                        Some(ResolutionTarget {
322                            candidate: applicable[0].clone(),
323                            strategy: ResolutionStrategy::UniqueCandidate,
324                        }),
325                        None,
326                    )
327                } else {
328                    (None, None)
329                }
330            }
331        };
332        let outcome = if target.is_some() {
333            ResolutionOutcome::Resolved
334        } else if scope.is_none()
335            && candidates
336                .iter()
337                .filter(|candidate| candidate.applicable)
338                .count()
339                > 1
340        {
341            ResolutionOutcome::Ambiguous
342        } else {
343            ResolutionOutcome::Missing
344        };
345
346        let mut unresolved = self
347            .unresolved()
348            .iter()
349            .filter(|reference| reference.symbol == symbol_name)
350            .cloned()
351            .collect::<Vec<_>>();
352        unresolved.sort_by(|left, right| {
353            left.module
354                .cmp(&right.module)
355                .then((left.kind as u8).cmp(&(right.kind as u8)))
356                .then(left.reason.cmp(&right.reason))
357        });
358
359        Ok(ResolutionTrace {
360            query: query.to_owned(),
361            symbol: symbol_name.to_owned(),
362            domain,
363            scope,
364            strictness: self.resolver_strictness(),
365            fallbacks,
366            candidates,
367            import,
368            outcome,
369            target,
370            unresolved,
371        })
372    }
373
374    fn unique_resolution_scope(
375        &self,
376        module_name: &str,
377    ) -> Result<ResolutionScope, ResolutionTraceError> {
378        let mut scopes = self
379            .modules_slice()
380            .iter()
381            .enumerate()
382            .filter(|(_, module)| module.name() == module_name)
383            .map(|(index, _)| self.resolution_scope(ModuleId::new(index as u32)))
384            .collect::<Vec<_>>();
385        scopes.sort_by(|left, right| {
386            left.source_label
387                .cmp(&right.source_label)
388                .then_with(|| right.last_updated.cmp(&left.last_updated))
389                .then(left.module.cmp(&right.module))
390        });
391        match scopes.len() {
392            0 => Err(ResolutionTraceError::ModuleNotFound(module_name.to_owned())),
393            1 => Ok(scopes.remove(0)),
394            _ => Err(ResolutionTraceError::AmbiguousModuleScope {
395                module: module_name.to_owned(),
396                candidates: scopes,
397            }),
398        }
399    }
400
401    fn resolution_scope(&self, module: ModuleId) -> ResolutionScope {
402        let handle = self.module_by_id(module);
403        ResolutionScope {
404            module,
405            module_name: handle.name().to_owned(),
406            source_label: handle.source_label().map(str::to_owned),
407            last_updated: handle.last_updated().to_owned(),
408        }
409    }
410
411    fn resolution_candidates(
412        &self,
413        name: &str,
414        domain: ResolutionDomain,
415    ) -> Vec<ResolutionCandidate> {
416        let mut candidates = self
417            .modules_slice()
418            .iter()
419            .enumerate()
420            .flat_map(|(index, module)| {
421                module.symbols(name).into_iter().map(move |symbol| {
422                    self.resolution_candidate(ModuleId::new(index as u32), symbol, domain, name)
423                })
424            })
425            .collect::<Vec<_>>();
426        candidates.sort_by(|left, right| {
427            left.module_name
428                .cmp(&right.module_name)
429                .then(left.source_label.cmp(&right.source_label))
430                .then_with(|| right.last_updated.cmp(&left.last_updated))
431                .then(left.kind.cmp(&right.kind))
432                .then(left.module.cmp(&right.module))
433        });
434        candidates
435    }
436
437    fn resolution_candidate(
438        &self,
439        module: ModuleId,
440        symbol: Symbol,
441        domain: ResolutionDomain,
442        name: &str,
443    ) -> ResolutionCandidate {
444        let handle = self.module_by_id(module);
445        ResolutionCandidate {
446            module,
447            module_name: handle.name().to_owned(),
448            source_label: handle.source_label().map(str::to_owned),
449            last_updated: handle.last_updated().to_owned(),
450            kind: symbol.into(),
451            symbol,
452            oid: symbol
453                .node(self)
454                .map(|node| self.node_by_id(node).oid().clone()),
455            applicable: symbol_matches_domain(symbol, domain, name),
456        }
457    }
458
459    fn resolve_trace_in_scope(
460        &self,
461        scope: ModuleId,
462        name: &str,
463        domain: ResolutionDomain,
464    ) -> (Option<ResolutionTarget>, Option<ImportResolution>) {
465        let fallback_domain = super::resolver::rules::fallback_domain(domain, name);
466
467        // Symbolic OID roots are recognized before ordinary module scope.
468        if domain == ResolutionDomain::Oid
469            && let Some(module_name) =
470                super::resolver::rules::intrinsic_foundation_module(fallback_domain, name)
471            && let Some(target) = self.foundation_candidate(module_name, name, domain)
472        {
473            let import = self.module_data(scope).import_resolution(name).cloned();
474            return (
475                Some(ResolutionTarget {
476                    candidate: target,
477                    strategy: ResolutionStrategy::IntrinsicFallback,
478                }),
479                import,
480            );
481        }
482
483        if let Some(local) = self.domain_candidate_in_module(scope, name, domain) {
484            return (
485                Some(ResolutionTarget {
486                    candidate: local,
487                    strategy: ResolutionStrategy::Local,
488                }),
489                None,
490            );
491        }
492
493        let import = self.module_data(scope).import_resolution(name).cloned();
494        if let Some(resolution) = &import
495            && let Some(target_module) = resolution.target
496            && let Some(imported) = self.domain_candidate_in_module(target_module, name, domain)
497        {
498            let strategy = match resolution.mode {
499                crate::mib::ImportResolutionMode::Direct => ResolutionStrategy::DirectImport,
500                crate::mib::ImportResolutionMode::Alias => ResolutionStrategy::AliasImport,
501                crate::mib::ImportResolutionMode::Forwarded => ResolutionStrategy::ForwardedImport,
502                crate::mib::ImportResolutionMode::Partial => ResolutionStrategy::PartialImport,
503                crate::mib::ImportResolutionMode::Unresolved
504                | crate::mib::ImportResolutionMode::Cycle => {
505                    unreachable!("an unresolved import cannot retain a target")
506                }
507            };
508            return (
509                Some(ResolutionTarget {
510                    candidate: imported,
511                    strategy,
512                }),
513                import,
514            );
515        }
516
517        if let Some(module_name) =
518            super::resolver::rules::intrinsic_foundation_module(fallback_domain, name)
519            && let Some(target) = self.foundation_candidate(module_name, name, domain)
520        {
521            return (
522                Some(ResolutionTarget {
523                    candidate: target,
524                    strategy: ResolutionStrategy::IntrinsicFallback,
525                }),
526                import,
527            );
528        }
529        for &module_name in super::resolver::rules::constrained_foundation_modules(
530            fallback_domain,
531            self.resolver_strictness(),
532        ) {
533            if let Some(target) = self.foundation_candidate(module_name, name, domain) {
534                return (
535                    Some(ResolutionTarget {
536                        candidate: target,
537                        strategy: ResolutionStrategy::ConstrainedFallback,
538                    }),
539                    import,
540                );
541            }
542        }
543        if super::resolver::rules::allows_global_fallback(
544            fallback_domain,
545            self.resolver_strictness(),
546        ) && let Some(target) = self.global_domain_candidate(name, domain)
547        {
548            return (
549                Some(ResolutionTarget {
550                    candidate: target,
551                    strategy: ResolutionStrategy::GlobalFallback,
552                }),
553                import,
554            );
555        }
556        (None, import)
557    }
558
559    fn domain_candidate_in_module(
560        &self,
561        module: ModuleId,
562        name: &str,
563        domain: ResolutionDomain,
564    ) -> Option<ResolutionCandidate> {
565        let data = self.module_data(module);
566        let symbol = match domain {
567            ResolutionDomain::Type => Symbol::Type(data.type_by_name(name)?),
568            ResolutionDomain::Oid
569            | ResolutionDomain::GroupMember
570            | ResolutionDomain::Conformance => data
571                .symbols(name)
572                .into_iter()
573                .find(|symbol| !matches!(symbol, Symbol::Type(_)))?,
574            ResolutionDomain::Object | ResolutionDomain::NotificationObject => {
575                Symbol::Object(data.object_by_name(name)?)
576            }
577            ResolutionDomain::Index if super::resolver::rules::is_bare_index_type(name) => {
578                Symbol::Type(data.type_by_name(name)?)
579            }
580            ResolutionDomain::Index => Symbol::Object(data.object_by_name(name)?),
581        };
582        Some(self.resolution_candidate(module, symbol, domain, name))
583    }
584
585    fn foundation_candidate(
586        &self,
587        module_name: &str,
588        name: &str,
589        domain: ResolutionDomain,
590    ) -> Option<ResolutionCandidate> {
591        let module = self
592            .modules_slice()
593            .iter()
594            .enumerate()
595            .rev()
596            .find(|(_, module)| module.name() == module_name)
597            .map(|(index, _)| ModuleId::new(index as u32))?;
598        self.domain_candidate_in_module(module, name, domain)
599    }
600
601    fn global_domain_candidate(
602        &self,
603        name: &str,
604        domain: ResolutionDomain,
605    ) -> Option<ResolutionCandidate> {
606        match domain {
607            ResolutionDomain::Object | ResolutionDomain::Index => {
608                if domain == ResolutionDomain::Index
609                    && super::resolver::rules::is_bare_index_type(name)
610                {
611                    return None;
612                }
613                let symbol = Symbol::Object(self.object_by_name(name)?);
614                let module = symbol.module(self)?;
615                Some(self.resolution_candidate(module, symbol, domain, name))
616            }
617            ResolutionDomain::GroupMember | ResolutionDomain::Conformance => {
618                for (index, module) in self.modules_slice().iter().enumerate() {
619                    if module.node_by_name(name).is_none() {
620                        continue;
621                    }
622                    return self.domain_candidate_in_module(
623                        ModuleId::new(index as u32),
624                        name,
625                        domain,
626                    );
627                }
628                None
629            }
630            ResolutionDomain::NotificationObject => {
631                for (index, module) in self.modules_slice().iter().enumerate() {
632                    let Some(node) = module.node_by_name(name) else {
633                        continue;
634                    };
635                    let symbol = self.symbol_for_resolved_node(node);
636                    if !matches!(symbol, Symbol::Object(_)) {
637                        return None;
638                    }
639                    let module = symbol.module(self).unwrap_or(ModuleId::new(index as u32));
640                    return Some(self.resolution_candidate(module, symbol, domain, name));
641                }
642                None
643            }
644            ResolutionDomain::Type | ResolutionDomain::Oid => None,
645        }
646    }
647
648    fn symbol_for_resolved_node(&self, node: crate::mib::NodeId) -> Symbol {
649        let data = self.node_data(node);
650        if let Some(object) = data.object {
651            Symbol::Object(object)
652        } else if let Some(notification) = data.notification {
653            Symbol::Notification(notification)
654        } else if let Some(group) = data.group {
655            Symbol::Group(group)
656        } else if let Some(compliance) = data.compliance {
657            Symbol::Compliance(compliance)
658        } else if let Some(capability) = data.capability {
659            Symbol::Capability(capability)
660        } else {
661            Symbol::Node(node)
662        }
663    }
664}
665
666fn symbol_matches_domain(symbol: Symbol, domain: ResolutionDomain, name: &str) -> bool {
667    match domain {
668        ResolutionDomain::Type => matches!(symbol, Symbol::Type(_)),
669        ResolutionDomain::Object | ResolutionDomain::NotificationObject => {
670            matches!(symbol, Symbol::Object(_))
671        }
672        ResolutionDomain::Index if super::resolver::rules::is_bare_index_type(name) => {
673            matches!(symbol, Symbol::Type(_))
674        }
675        ResolutionDomain::Index => matches!(symbol, Symbol::Object(_)),
676        ResolutionDomain::Oid | ResolutionDomain::GroupMember | ResolutionDomain::Conformance => {
677            !matches!(symbol, Symbol::Type(_))
678        }
679    }
680}
681
682fn parse_query(query: &str) -> Result<(Option<&str>, &str), ResolutionTraceError> {
683    if query.is_empty() {
684        return Err(ResolutionTraceError::EmptyQuery);
685    }
686    let Some((module, symbol)) = query.split_once("::") else {
687        return Ok((None, query));
688    };
689    if module.is_empty() || symbol.is_empty() || symbol.contains("::") {
690        return Err(ResolutionTraceError::InvalidQualifiedQuery(
691            query.to_owned(),
692        ));
693    }
694    Ok((Some(module), symbol))
695}