Skip to main content

pedant_core/project/
shape.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::rc::Rc;
3use std::sync::Arc;
4
5use crate::check_config::CheckConfig;
6use crate::ir::{FileIr, FnFact, IrSpan};
7
8/// Where a type name is defined, for the one-definition-site identity rule.
9#[derive(Debug, Clone)]
10pub struct TypeDefSite {
11    /// Name of the defined type, as written.
12    pub type_name: Box<str>,
13    /// Location of the definition keyword.
14    pub span: IrSpan,
15}
16
17/// One type's inherent-impl footprint within a single file, under one set of
18/// `#[cfg]` predicates.
19///
20/// A file contributes one site per `(type, predicate set)` pair, because only
21/// impls sharing a predicate set are guaranteed to compile into the same build.
22#[derive(Debug, Clone)]
23pub struct InherentImplSite {
24    /// Name of the implemented type, as written.
25    pub type_name: Box<str>,
26    /// Sorted, deduplicated `#[cfg]` predicates guarding these impls within the
27    /// file. Empty means they are in every build of the file.
28    pub cfg_predicates: Box<[Box<str>]>,
29    /// Location of the first inherent `impl` for the type under these gates.
30    pub first_impl: IrSpan,
31    /// Inherent methods contributed here, after forwarder exclusion.
32    pub method_count: usize,
33}
34
35/// A `mod` declaration guarded by `#[cfg(…)]`.
36///
37/// The gate is only ever written in the *parent* file, so it is invisible from
38/// inside the module's own file. Carrying it here is what lets the project pass
39/// attribute `unix.rs` to `#[cfg(unix)]`.
40#[derive(Debug, Clone)]
41pub struct CfgGatedModule {
42    /// Module name, which names the file or directory it pulls in.
43    pub name: Box<str>,
44    /// Sorted, deduplicated predicates guarding the declaration.
45    pub cfg_predicates: Box<[Box<str>]>,
46}
47
48/// The slice of a file's IR that whole-crate checks need.
49///
50/// This is a projection, not a copy: only type definition sites, per-type
51/// inherent-impl footprints, and `#[cfg]`-gated `mod` declarations survive, so
52/// the project pass never holds a [`FileIr`] and never re-parses a file.
53#[derive(Debug, Clone)]
54pub struct FileShape {
55    /// Path of the file this shape was projected from.
56    pub file_path: Arc<str>,
57    /// Every struct, enum, union, and trait defined in this file.
58    pub type_defs: Box<[TypeDefSite]>,
59    /// Per-type, per-predicate-set inherent-impl footprints.
60    pub inherent_impls: Box<[InherentImplSite]>,
61    /// `mod` declarations in this file carrying a `#[cfg(…)]`.
62    pub cfg_gated_modules: Box<[CfgGatedModule]>,
63}
64
65/// Project the facts whole-crate checks need out of a file's IR.
66pub fn project_shape(ir: &FileIr, config: &CheckConfig) -> FileShape {
67    FileShape {
68        file_path: Arc::clone(&ir.file_path),
69        type_defs: collect_type_defs(ir),
70        inherent_impls: collect_inherent_impls(ir, config),
71        cfg_gated_modules: collect_cfg_gated_modules(ir),
72    }
73}
74
75/// Canonical form of a predicate set: sorted and deduplicated, so two items
76/// under the same gates produce equal keys.
77pub(super) fn canonical_predicates<'a>(
78    predicates: impl IntoIterator<Item = &'a str>,
79) -> Box<[Box<str>]> {
80    predicates
81        .into_iter()
82        .collect::<BTreeSet<_>>()
83        .into_iter()
84        .map(Box::from)
85        .collect()
86}
87
88fn predicate_key(predicates: &[Rc<str>]) -> Box<[Box<str>]> {
89    canonical_predicates(predicates.iter().map(|predicate| &**predicate))
90}
91
92/// Every type definition in the file, of every kind.
93///
94/// Kind is deliberately dropped. A name that is both a `struct` and a `trait`
95/// is two definition sites, which is exactly the ambiguity that must suppress a
96/// finding rather than be resolved by guessing.
97fn collect_type_defs(ir: &FileIr) -> Box<[TypeDefSite]> {
98    ir.type_defs
99        .iter()
100        .map(|def| TypeDefSite {
101            type_name: Box::from(&*def.name),
102            span: def.span,
103        })
104        .collect()
105}
106
107/// Key identifying one `(type, predicate set)` footprint within a file.
108type SiteKey<'a> = (&'a str, Box<[Box<str>]>);
109
110fn collect_inherent_impls(ir: &FileIr, config: &CheckConfig) -> Box<[InherentImplSite]> {
111    let mut sites: BTreeMap<SiteKey<'_>, (IrSpan, usize)> = BTreeMap::new();
112    seed_impl_blocks(ir, &mut sites);
113    count_methods(ir, config, &mut sites);
114    sites
115        .into_iter()
116        .map(
117            |((type_name, cfg_predicates), (first_impl, method_count))| InherentImplSite {
118                type_name: Box::from(type_name),
119                cfg_predicates,
120                first_impl,
121                method_count,
122            },
123        )
124        .collect()
125}
126
127/// Record each inherent `impl` block, so a type reads as present under its
128/// gates even when the block is empty or holds only forwarders.
129fn seed_impl_blocks<'a>(ir: &'a FileIr, sites: &mut BTreeMap<SiteKey<'a>, (IrSpan, usize)>) {
130    let inherent = ir.impl_blocks.iter().filter(|imp| imp.trait_name.is_none());
131    for imp in inherent {
132        sites
133            .entry((&imp.self_type, predicate_key(&imp.cfg_predicates)))
134            .or_insert((imp.span, 0));
135    }
136}
137
138/// Add each method to the footprint for its own gates, which may be stricter
139/// than its `impl` block's when the method carries a `#[cfg]` of its own.
140fn count_methods<'a>(
141    ir: &'a FileIr,
142    config: &CheckConfig,
143    sites: &mut BTreeMap<SiteKey<'a>, (IrSpan, usize)>,
144) {
145    for func in &ir.functions {
146        let Some(type_name) = &func.inherent_method_of else {
147            continue;
148        };
149        if !counts_toward_surface(func, config) {
150            continue;
151        }
152        let entry = sites
153            .entry((&**type_name, predicate_key(&func.cfg_predicates)))
154            .or_insert((func.span, 0));
155        entry.1 += 1;
156    }
157}
158
159/// Pure forwarders carry no responsibility of their own, matching
160/// `high-method-count`. Conditional methods are *not* excluded here — the
161/// project pass groups them by predicate instead, because excluding them would
162/// let a `#[cfg]` on a default-on feature hide a god-object that ships.
163fn counts_toward_surface(func: &FnFact, config: &CheckConfig) -> bool {
164    !func.is_pure_forwarder || config.count_forwarders
165}
166
167fn collect_cfg_gated_modules(ir: &FileIr) -> Box<[CfgGatedModule]> {
168    ir.modules
169        .iter()
170        .filter(|module| !module.cfg_predicates.is_empty())
171        .map(|module| CfgGatedModule {
172            name: module.name.clone(),
173            cfg_predicates: predicate_key(&module.cfg_predicates),
174        })
175        .collect()
176}