Skip to main content

cabin_build/
standard_compat.rs

1//! Post-resolution language-standard compatibility checking.
2//!
3//! Evaluates the edge-compatibility model of
4//! `docs/design/standard-compatibility/spec.md` over the planner's
5//! resolved target graph: per-target `ReqOf` attributes are mapped
6//! per the spec's D6 population contract, `cabin_workspace::standards`
7//! composes the effective requirement `R_L` along public edges (D10),
8//! and every resolved dependency edge is checked per D13 for every
9//! language the consumer compiles.  Violated edges become
10//! [`StandardCompatViolation`] records on the [`crate::BuildGraph`];
11//! the CLI renders them as errors that fail the command.  An edge
12//! whose consuming package declares `ignore-interface-standard =
13//! true` on the matching `[dependencies]` entry is still evaluated,
14//! but its violations are marked [`StandardCompatViolation::ignored`]
15//! so the CLI downgrades them to unchecked-edge notes.
16//!
17//! The pass runs strictly after resolution (fresh or
18//! lockfile-seeded - both produce the same loaded graph) and never
19//! feeds back into version selection.  Its defaults deliberately
20//! differ from the build-time interface enforcement in
21//! `planner::enforce_interface_standards`: a compiled dependency
22//! with no interface declaration imposes nothing here (spec D9 row
23//! 4 - no implementation-standard fallback), while an explicit
24//! `"none"` is unsatisfiable (row 1).
25
26use std::collections::HashMap;
27use std::path::PathBuf;
28
29use cabin_core::standard_compatibility::{DependencyAttributes, ReqOfSource, Requirement};
30use cabin_core::{
31    LanguageStandardSettings, LanguageStandardSource, ResolvedLanguageStandards, SourceLanguage,
32    StandardDeclaration, Target, classify_source, effective_c, effective_cxx,
33};
34use cabin_workspace::PackageKind;
35use cabin_workspace::standards::{
36    DeclarationSite, DeclarationSites, Provenance, TargetEdge, TargetNode, effective_requirements,
37    provenance_c, provenance_cxx,
38};
39
40use crate::error::BuildError;
41use crate::planner::{PlanRequest, TargetDepEdge, TargetId, format_target_id, lookup_target};
42
43/// Which manifest table a cited declaration lives in.  Together
44/// with [`DeclSite::field`] this is enough for a renderer to
45/// re-locate the declaration's span in the manifest text.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum DeclScope {
48    /// The `[package]` table.
49    Package,
50    /// The `[target.<name>]` table of the carried target.
51    Target(String),
52    /// The `[workspace]` table of the workspace root manifest
53    /// (the value was inherited via `{ workspace = true }`).
54    Workspace,
55}
56
57/// A manifest declaration a violation cites: file, table, and field.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DeclSite {
60    /// Manifest that carries the declaration.  The workspace root
61    /// manifest for [`DeclScope::Workspace`], the declaring
62    /// package's manifest otherwise.
63    pub manifest_path: PathBuf,
64    pub scope: DeclScope,
65    /// Manifest field name (`c-standard`, `interface-cxx-standard`, ...).
66    pub field: &'static str,
67}
68
69/// Why a dependency's effective requirement is what it is - the D9
70/// row that originates the join, with the declaration to cite.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum RequirementOrigin {
73    /// Row 2: an explicit `interface-*-standard` minimum.
74    Declared { site: DeclSite },
75    /// Row 1: `interface-*-standard = "none"` - consumption from
76    /// this language was disabled by the origin target's author.
77    DeclaredNone { site: DeclSite },
78    /// Row 3: a header-only target's minimum inferred from its
79    /// implementation standard.
80    HeaderOnlyInference { site: DeclSite },
81    /// Row 6: the strict C++-to-C default - the origin target
82    /// implements no C and declares no C interface, so there is no
83    /// declaration to cite.
84    CrossLanguageDefault,
85}
86
87/// The violated effective requirement `R_L(d)` of spec D13.
88/// `Unconstrained` cannot be violated, so it has no variant here.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum EdgeRequirement {
91    /// A minimum consumer level (`[m]` of spec D3).
92    Min(&'static str),
93    /// Unsatisfiable at every consumer level.
94    Forbidden,
95}
96
97/// One resolved dependency edge that violates spec D13 for one
98/// consumer language.  A mixed-language consumer failing both
99/// languages on the same edge yields two records.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct StandardCompatViolation {
102    /// `package:target` of the consuming side of the edge.
103    pub consumer: String,
104    /// The consumer language whose conjunct of D13 failed.
105    pub language: SourceLanguage,
106    /// The consumer's effective compile level for `language`.
107    pub consumer_standard: &'static str,
108    /// Where the consumer's standard is declared.
109    pub consumer_site: DeclSite,
110    /// `package:target` of the direct dependency the edge points at.
111    pub dependency: String,
112    /// The dependency's package name, for the pin remedy.
113    pub dependency_package: String,
114    /// The dependency's package version, for the pin remedy.
115    pub dependency_version: String,
116    /// Whether the dependency came from a registry - the pin
117    /// remedy only applies to versioned dependencies.
118    pub dependency_is_registry: bool,
119    /// The violated effective requirement `R_L(dependency)`.
120    pub requirement: EdgeRequirement,
121    /// `package:target` whose own `ReqOf` originates the
122    /// requirement (spec T1's closed form); equals `dependency`
123    /// when the dependency itself declares it.
124    pub origin_target: String,
125    /// The D9 row and declaration behind `origin_target`'s `ReqOf`.
126    pub origin: RequirementOrigin,
127    /// Public-edge provenance chain from `dependency` down to
128    /// `origin_target`, `package:target` names inclusive on both
129    /// ends; a single entry when the dependency's own declaration
130    /// attains the join.
131    pub chain: Vec<String>,
132    /// Manifest of the consuming target's own package - where a
133    /// per-edge `ignore-interface-standard = true` override would
134    /// be declared.  Distinct from
135    /// [`StandardCompatViolation::consumer_site`], which points at
136    /// the workspace root manifest for inherited standards.
137    pub consumer_manifest_path: PathBuf,
138    /// Whether the consuming package opted this edge out of the
139    /// check with `ignore-interface-standard = true` on the
140    /// selected dependency entry.  The CLI renders ignored
141    /// violations as unchecked-edge notes instead of errors.
142    pub ignored: bool,
143    /// Manifest table of the package-dependency entry that
144    /// resolved this edge - where a per-edge override lives or
145    /// would live: `[dependencies]`, `[dev-dependencies]`, or the
146    /// condition-qualified `[target.'cfg(...)'.<kind>]` form the
147    /// entry was declared under.  `None` for intra-package edges,
148    /// which have no entry to carry one.
149    pub override_section: Option<String>,
150}
151
152/// Per-node declaration-site bookkeeping, parallel to the
153/// [`TargetNode`] slice: the [`DeclSite`] behind each populated D6
154/// attribute, for [`RequirementOrigin`] construction.
155struct NodeSites {
156    decl_c: Option<DeclSite>,
157    decl_cxx: Option<DeclSite>,
158    impl_c: Option<DeclSite>,
159    impl_cxx: Option<DeclSite>,
160}
161
162/// Evaluate spec D13 over every resolved dependency edge and return
163/// the violated (edge, language) pairs, sorted by consumer,
164/// dependency, then language for deterministic output.
165///
166/// # Errors
167/// Returns [`BuildError::UnknownTargetInPackage`] when an edge
168/// references a target its package does not declare - unreachable
169/// after the planner's own resolution walk, kept as an error for
170/// symmetry with the surrounding planner code.
171pub(crate) fn edge_violations(
172    topo: &[TargetId],
173    resolved_deps: &HashMap<TargetId, Vec<TargetDepEdge>>,
174    req: &PlanRequest<'_>,
175) -> Result<Vec<StandardCompatViolation>, BuildError> {
176    let index_of: HashMap<&TargetId, usize> = topo
177        .iter()
178        .enumerate()
179        .map(|(index, tid)| (tid, index))
180        .collect();
181
182    let mut nodes: Vec<TargetNode> = Vec::with_capacity(topo.len());
183    let mut sites: Vec<NodeSites> = Vec::with_capacity(topo.len());
184    for tid in topo {
185        let target = lookup_target(tid, req.graph)?;
186        let pkg = &req.graph.packages[tid.0];
187        let pkg_standards = req
188            .language_standards
189            .get(&tid.0)
190            .copied()
191            .unwrap_or_default();
192        let (attributes, node_sites) =
193            dependency_attributes(tid, target, pkg_standards, &pkg.package.language, req);
194        let deps = resolved_deps
195            .get(tid)
196            .map(|edges| {
197                edges
198                    .iter()
199                    .map(|edge| TargetEdge {
200                        to: index_of[&edge.to],
201                        public: edge.public,
202                    })
203                    .collect()
204            })
205            .unwrap_or_default();
206        nodes.push(TargetNode {
207            name: format_target_id(tid, req.graph),
208            manifest_path: pkg.manifest_path.clone(),
209            attributes,
210            sites: declaration_sites(&node_sites),
211            deps,
212        });
213        sites.push(node_sites);
214    }
215
216    let effective = effective_requirements(&nodes);
217
218    let mut violations = Vec::new();
219    for (consumer_index, tid) in topo.iter().enumerate() {
220        let consumer_target = lookup_target(tid, req.graph)?;
221        // A header-only consumer compiles no language: every edge
222        // out of it is compatible vacuously (spec D13); its
223        // requirements still propagated through `effective`.
224        if consumer_target.kind.is_header_only() {
225            continue;
226        }
227        let pkg_standards = req
228            .language_standards
229            .get(&tid.0)
230            .copied()
231            .unwrap_or_default();
232        let Some(edges) = resolved_deps.get(tid) else {
233            continue;
234        };
235        let compiles_c = has_sources_of(consumer_target, SourceLanguage::C);
236        let compiles_cxx = has_sources_of(consumer_target, SourceLanguage::Cxx);
237        // Which package-dependency edge resolved this target's
238        // deps - the planner's own lookup rule
239        // (`resolve_target_dep`): Normal-kind edges win, and
240        // Dev-kind edges participate only for dev-only target
241        // kinds (`test` / `example`) and only when no Normal edge
242        // to the package exists.  The opt-out must sit on the
243        // entry resolution actually selected: a flag on an
244        // unselected (or invisible) edge suppresses nothing.
245        let dev_edges_visible = consumer_target.kind.is_dev_only();
246        let selected_pkg_edge = |dep_pkg: usize| -> Option<&cabin_workspace::DependencyEdge> {
247            let pkg_edges = &req.graph.packages[tid.0].deps;
248            pkg_edges
249                .iter()
250                .find(|pkg_edge| {
251                    pkg_edge.kind == cabin_core::DependencyKind::Normal && pkg_edge.index == dep_pkg
252                })
253                .or_else(|| {
254                    dev_edges_visible
255                        .then(|| {
256                            pkg_edges.iter().find(|pkg_edge| {
257                                pkg_edge.kind == cabin_core::DependencyKind::Dev
258                                    && pkg_edge.index == dep_pkg
259                            })
260                        })
261                        .flatten()
262                })
263        };
264        for edge in edges {
265            let dep_index = index_of[&edge.to];
266            // Per-edge opt-out: the consuming package's own
267            // dependency entry for the dependency's package
268            // carries `ignore-interface-standard = true`.  The
269            // check still runs so the CLI can report the edge as
270            // unchecked; only cross-package edges are overridable
271            // (an intra-package edge has no `[dependencies]`
272            // entry to carry the flag).  The selected entry's
273            // manifest section travels on the violation so the
274            // remedies name the table resolution actually read.
275            let selected = if tid.0 == edge.to.0 {
276                None
277            } else {
278                selected_pkg_edge(edge.to.0)
279            };
280            let edge_ignored = selected.is_some_and(|pkg_edge| pkg_edge.ignore_interface_standard);
281            let override_section = selected.map(section_of);
282            // D13's conjunction ranges over the languages the
283            // consumer compiles; an absent effective standard for a
284            // compiled language is a manifest error surfaced at the
285            // compile site, not here.
286            if compiles_c
287                && let Some(consumer) = effective_c(&pkg_standards, consumer_target)
288                && !effective[dep_index]
289                    .c
290                    .requirement
291                    .satisfied_by(consumer.standard)
292            {
293                let provenance = provenance_c(&effective, dep_index);
294                if !interface_less_default_origin(&provenance, topo, req)? {
295                    violations.push(violation(
296                        SourceLanguage::C,
297                        consumer.standard.as_str(),
298                        consumer_site(consumer.source, "c-standard", consumer_index, &nodes, req),
299                        requirement_of(effective[dep_index].c.requirement),
300                        &provenance,
301                        tid,
302                        edge,
303                        edge_ignored,
304                        override_section.clone(),
305                        &nodes,
306                        &sites,
307                        req,
308                    ));
309                }
310            }
311            if compiles_cxx
312                && let Some(consumer) = effective_cxx(&pkg_standards, consumer_target)
313                && !effective[dep_index]
314                    .cxx
315                    .requirement
316                    .satisfied_by(consumer.standard)
317            {
318                let provenance = provenance_cxx(&effective, dep_index);
319                if !interface_less_default_origin(&provenance, topo, req)? {
320                    violations.push(violation(
321                        SourceLanguage::Cxx,
322                        consumer.standard.as_str(),
323                        consumer_site(consumer.source, "cxx-standard", consumer_index, &nodes, req),
324                        requirement_of(effective[dep_index].cxx.requirement),
325                        &provenance,
326                        tid,
327                        edge,
328                        edge_ignored,
329                        override_section.clone(),
330                        &nodes,
331                        &sites,
332                        req,
333                    ));
334                }
335            }
336        }
337    }
338
339    // Topo iteration is already deterministic for a fixed graph;
340    // the explicit sort pins the reading order to something a user
341    // can predict regardless of the topo tie-breaks.
342    violations.sort_by(|a, b| {
343        (&a.consumer, &a.dependency, a.language.human_label()).cmp(&(
344            &b.consumer,
345            &b.dependency,
346            b.language.human_label(),
347        ))
348    });
349    Ok(violations)
350}
351
352/// The manifest table a package-dependency edge was declared
353/// under, exactly as a user would spell it: the plain kind table
354/// for unconditional entries, the condition-qualified
355/// `[target.'cfg(...)'.<kind>]` form otherwise.  Remedies must
356/// name the declaring table - `Package::validate_dependencies`
357/// rejects a second same-kind entry for the same package, so
358/// pointing a conditional edge's user at the top-level table
359/// would send them into a rejected manifest.
360fn section_of(pkg_edge: &cabin_workspace::DependencyEdge) -> String {
361    let table = match pkg_edge.kind {
362        cabin_core::DependencyKind::Normal => "dependencies",
363        cabin_core::DependencyKind::Dev => "dev-dependencies",
364    };
365    match &pkg_edge.condition {
366        Some(condition) => format!("[target.'cfg({condition})'.{table}]"),
367        None => format!("[{table}]"),
368    }
369}
370
371/// Whether a violated requirement originates at an interface-less
372/// target's cross-language default and should be suppressed.
373///
374/// An executable-like target has no consumable interface: the
375/// manifest rejects `interface-*` fields on those kinds, and the
376/// planner's include-dir walk never takes headers from them.  The
377/// strict C++-to-C default (spec D9 row 6) presumes dependency
378/// headers a C consumer could fail to compile, so a requirement the
379/// default originates *at* such a target warns about headers that
380/// do not exist.  Requirements merely passing through it from a
381/// library behind it keep warning (their origin is the library),
382/// and the always-on build-time enforcement independently checks
383/// the transitive closure, so nothing real is lost.
384fn interface_less_default_origin(
385    provenance: &Provenance<'_>,
386    topo: &[TargetId],
387    req: &PlanRequest<'_>,
388) -> Result<bool, BuildError> {
389    // Rows 1-3 cannot originate at an executable-like target (the
390    // parser rejects its interface fields, the package-level
391    // default is gated to library-like kinds, and it is not
392    // header-only), so the cross-language default is the only
393    // origin to inspect.
394    if provenance.origin.source != ReqOfSource::CrossLanguageDefault {
395        return Ok(false);
396    }
397    let origin_tid = &topo[*provenance
398        .path
399        .last()
400        .expect("a provenance chain is never empty")];
401    let origin = lookup_target(origin_tid, req.graph)?;
402    Ok(!origin.kind.is_library_like())
403}
404
405/// Spec D6 attribute mapping for one target, with the declaration
406/// sites behind each populated attribute.
407///
408/// Population contract (D6): `impl_L` is `Some` exactly when the
409/// target itself implements `L` - a compiled target implements `L`
410/// when it has sources of `L` (level via target-over-package
411/// precedence), a header-only target only via a target-level
412/// implementation declaration.  `decl_L` is the explicit interface
413/// declaration only (target over package tier, workspace-inherited
414/// counts as declared) - never the build-time implementation-
415/// standard fallback.
416fn dependency_attributes(
417    tid: &TargetId,
418    target: &Target,
419    pkg_standards: ResolvedLanguageStandards,
420    pkg_settings: &LanguageStandardSettings,
421    req: &PlanRequest<'_>,
422) -> (DependencyAttributes, NodeSites) {
423    // The D6 attribute mapping (population contract included) is shared
424    // with the published-index derivation so the two cannot drift; only
425    // the declaration-site bookkeeping for diagnostics is local here.
426    let attributes = cabin_core::standard_compatibility::dependency_attributes(
427        target,
428        &pkg_standards,
429        pkg_settings,
430    );
431    let header_only = target.kind.is_header_only();
432
433    let pkg = &req.graph.packages[tid.0];
434    let target_name = target.name.as_str();
435
436    let node_sites = NodeSites {
437        decl_c: attributes.decl_c.is_some().then(|| {
438            interface_decl_site(
439                "interface-c-standard",
440                target.language.interface_c_standard.is_some(),
441                matches!(
442                    pkg_settings.interface_c_standard,
443                    Some(StandardDeclaration::Inherited(_))
444                ),
445                target_name,
446                pkg,
447                req,
448            )
449        }),
450        decl_cxx: attributes.decl_cxx.is_some().then(|| {
451            interface_decl_site(
452                "interface-cxx-standard",
453                target.language.interface_cxx_standard.is_some(),
454                matches!(
455                    pkg_settings.interface_cxx_standard,
456                    Some(StandardDeclaration::Inherited(_))
457                ),
458                target_name,
459                pkg,
460                req,
461            )
462        }),
463        // Header-only inference (D9 row 3) cites the target-level
464        // implementation declaration the inference read; a compiled
465        // target's implementation standard is never cited (row 4
466        // imposes nothing).
467        impl_c: (header_only && attributes.impl_c.is_some()).then(|| DeclSite {
468            manifest_path: pkg.manifest_path.clone(),
469            scope: DeclScope::Target(target_name.to_owned()),
470            field: "c-standard",
471        }),
472        impl_cxx: (header_only && attributes.impl_cxx.is_some()).then(|| DeclSite {
473            manifest_path: pkg.manifest_path.clone(),
474            scope: DeclScope::Target(target_name.to_owned()),
475            field: "cxx-standard",
476        }),
477    };
478
479    (attributes, node_sites)
480}
481
482/// The [`DeclSite`] of a populated interface declaration: the
483/// target-level field when present, otherwise the package-level
484/// field (which points at the workspace root when inherited).
485fn interface_decl_site(
486    field: &'static str,
487    target_declares: bool,
488    pkg_inherited: bool,
489    target_name: &str,
490    pkg: &cabin_workspace::WorkspacePackage,
491    req: &PlanRequest<'_>,
492) -> DeclSite {
493    if target_declares {
494        DeclSite {
495            manifest_path: pkg.manifest_path.clone(),
496            scope: DeclScope::Target(target_name.to_owned()),
497            field,
498        }
499    } else if pkg_inherited {
500        DeclSite {
501            manifest_path: req.graph.root_manifest_path.clone(),
502            scope: DeclScope::Workspace,
503            field,
504        }
505    } else {
506        DeclSite {
507            manifest_path: pkg.manifest_path.clone(),
508            scope: DeclScope::Package,
509            field,
510        }
511    }
512}
513
514/// Project [`NodeSites`] into the spanless [`DeclarationSites`] the
515/// effective-requirement composition records provenance with.
516fn declaration_sites(sites: &NodeSites) -> DeclarationSites {
517    let site = |decl: &Option<DeclSite>| {
518        decl.as_ref().map(|s| DeclarationSite {
519            manifest_path: s.manifest_path.clone(),
520            span: None,
521        })
522    };
523    DeclarationSites {
524        decl_c: site(&sites.decl_c),
525        decl_cxx: site(&sites.decl_cxx),
526        impl_c: site(&sites.impl_c),
527        impl_cxx: site(&sites.impl_cxx),
528    }
529}
530
531/// The consumer-side [`DeclSite`] for the violated language's
532/// effective compile standard.
533fn consumer_site(
534    source: LanguageStandardSource,
535    field: &'static str,
536    consumer_index: usize,
537    nodes: &[TargetNode],
538    req: &PlanRequest<'_>,
539) -> DeclSite {
540    let manifest_path = nodes[consumer_index].manifest_path.clone();
541    match source {
542        LanguageStandardSource::Target => DeclSite {
543            manifest_path,
544            scope: DeclScope::Target(target_name_of(&nodes[consumer_index].name)),
545            field,
546        },
547        LanguageStandardSource::Package => DeclSite {
548            manifest_path,
549            scope: DeclScope::Package,
550            field,
551        },
552        LanguageStandardSource::Workspace => DeclSite {
553            manifest_path: req.graph.root_manifest_path.clone(),
554            scope: DeclScope::Workspace,
555            field,
556        },
557    }
558}
559
560/// The `target` half of a `package:target` display name.
561fn target_name_of(display: &str) -> String {
562    display
563        .split_once(':')
564        .map_or(display, |(_, target)| target)
565        .to_owned()
566}
567
568fn requirement_of<S: Copy + Ord + AsStandardStr>(requirement: Requirement<S>) -> EdgeRequirement {
569    match requirement {
570        Requirement::Min(min) => EdgeRequirement::Min(min.standard_str()),
571        Requirement::Forbidden => EdgeRequirement::Forbidden,
572        // A satisfied requirement never reaches violation
573        // construction: `unconstrained` is satisfied at every level
574        // (spec D11).
575        Requirement::Unconstrained => {
576            unreachable!("an unconstrained requirement cannot be violated (spec D11)")
577        }
578    }
579}
580
581/// `as_str` unification for the two level chains, local to this
582/// module so `requirement_of` can stay generic like the spec's `L`.
583trait AsStandardStr {
584    fn standard_str(self) -> &'static str;
585}
586impl AsStandardStr for cabin_core::CStandard {
587    fn standard_str(self) -> &'static str {
588        self.as_str()
589    }
590}
591impl AsStandardStr for cabin_core::CxxStandard {
592    fn standard_str(self) -> &'static str {
593        self.as_str()
594    }
595}
596
597#[allow(clippy::too_many_arguments)]
598fn violation(
599    language: SourceLanguage,
600    consumer_standard: &'static str,
601    consumer_site: DeclSite,
602    requirement: EdgeRequirement,
603    provenance: &Provenance<'_>,
604    consumer_tid: &TargetId,
605    edge: &TargetDepEdge,
606    ignored: bool,
607    override_section: Option<String>,
608    nodes: &[TargetNode],
609    sites: &[NodeSites],
610    req: &PlanRequest<'_>,
611) -> StandardCompatViolation {
612    let origin_index = *provenance
613        .path
614        .last()
615        .expect("a provenance chain is never empty");
616    let origin_sites = &sites[origin_index];
617    let origin = match provenance.origin.source {
618        ReqOfSource::Declared => RequirementOrigin::Declared {
619            site: decl_site_for(origin_sites, language),
620        },
621        ReqOfSource::DeclaredNone => RequirementOrigin::DeclaredNone {
622            site: decl_site_for(origin_sites, language),
623        },
624        ReqOfSource::HeaderOnlyInference => RequirementOrigin::HeaderOnlyInference {
625            site: impl_site_for(origin_sites, language),
626        },
627        ReqOfSource::CrossLanguageDefault => RequirementOrigin::CrossLanguageDefault,
628        // Row 4 yields `unconstrained`, which satisfies every
629        // consumer (spec D11): it can never originate a violated
630        // join.
631        ReqOfSource::CompiledNoDeclaration => unreachable!(
632            "a compiled target without a declaration imposes no constraint (spec D9 row 4)"
633        ),
634    };
635    let dep_pkg = &req.graph.packages[edge.to.0];
636    StandardCompatViolation {
637        consumer: format_target_id(consumer_tid, req.graph),
638        language,
639        consumer_standard,
640        consumer_site,
641        dependency: nodes[*provenance
642            .path
643            .first()
644            .expect("a provenance chain is never empty")]
645        .name
646        .clone(),
647        dependency_package: dep_pkg.package.name.as_str().to_owned(),
648        dependency_version: dep_pkg.package.version.to_string(),
649        dependency_is_registry: matches!(dep_pkg.kind, PackageKind::Registry),
650        requirement,
651        origin_target: nodes[origin_index].name.clone(),
652        origin,
653        chain: provenance
654            .path
655            .iter()
656            .map(|&index| nodes[index].name.clone())
657            .collect(),
658        consumer_manifest_path: req.graph.packages[consumer_tid.0].manifest_path.clone(),
659        ignored,
660        override_section,
661    }
662}
663
664/// The interface-declaration site of the origin target for
665/// `language`.  Present whenever the origin's `ReqOf` came from D9
666/// rows 1-2, which is exactly when this is called.
667fn decl_site_for(sites: &NodeSites, language: SourceLanguage) -> DeclSite {
668    match language {
669        SourceLanguage::C => sites.decl_c.clone(),
670        SourceLanguage::Cxx => sites.decl_cxx.clone(),
671    }
672    .expect("a declared requirement records its declaration site")
673}
674
675/// The implementation-declaration site anchoring header-only
676/// inference (D9 row 3).
677fn impl_site_for(sites: &NodeSites, language: SourceLanguage) -> DeclSite {
678    match language {
679        SourceLanguage::C => sites.impl_c.clone(),
680        SourceLanguage::Cxx => sites.impl_cxx.clone(),
681    }
682    .expect("header-only inference records its implementation site")
683}
684
685fn has_sources_of(target: &Target, language: SourceLanguage) -> bool {
686    target
687        .sources
688        .iter()
689        .any(|source| classify_source(source) == Some(language))
690}