use syn::visit::Visit;
use super::exposure::*;
use crate::finding::*;
use crate::resolve::*;
use crate::syn_util::*;
pub(crate) fn dyns_in_signature(sig: &syn::Signature) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
c.visit_signature(sig);
c.exposures
}
pub(crate) fn dyns_in_type(ty: &syn::Type) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
c.visit_type(ty);
c.exposures
}
pub(crate) fn dyns_in_generics(generics: &syn::Generics) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
c.visit_generics(generics);
c.exposures
}
pub(crate) fn dyns_in_bounds(
bounds: &syn::punctuated::Punctuated<syn::TypeParamBound, syn::token::Plus>,
) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
for bound in bounds {
c.visit_type_param_bound(bound);
}
c.exposures
}
pub(crate) fn collect_item_dyn_exposures(
item: &syn::Item,
module: &str,
uses: &UseMap,
ordinal: usize,
out: &mut Vec<ShapeExposure>,
) {
match item {
syn::Item::Fn(item) if is_public(&item.vis) => {
let seam = fn_seam(module, &item.sig.ident);
out.extend(stamp_seam(dyns_in_signature(&item.sig), &seam));
}
syn::Item::Struct(item) if is_public(&item.vis) => {
let name = strip_raw(&item.ident.to_string());
out.extend(stamp_seam(
dyns_in_generics(&item.generics),
&item_seam(ItemKind::Struct, module, &item.ident),
));
collect_named_field_exposures(
item.fields.iter(),
MemberKind::Field,
module,
&name,
|field| is_public(&field.vis),
|ty, seam| stamp_seam(dyns_in_type(ty), seam),
out,
);
}
syn::Item::Enum(item) if is_public(&item.vis) => {
let name = strip_raw(&item.ident.to_string());
out.extend(stamp_seam(
dyns_in_generics(&item.generics),
&item_seam(ItemKind::Enum, module, &item.ident),
));
for variant in &item.variants {
let owner = format!("{name}::{}", strip_raw(&variant.ident.to_string()));
collect_named_field_exposures(
variant.fields.iter(),
MemberKind::Variant,
module,
&owner,
|_| true,
|ty, seam| stamp_seam(dyns_in_type(ty), seam),
out,
);
}
}
syn::Item::Union(item) if is_public(&item.vis) => {
let name = strip_raw(&item.ident.to_string());
out.extend(stamp_seam(
dyns_in_generics(&item.generics),
&item_seam(ItemKind::Union, module, &item.ident),
));
collect_named_field_exposures(
item.fields.named.iter(),
MemberKind::Field,
module,
&name,
|field| is_public(&field.vis),
|ty, seam| stamp_seam(dyns_in_type(ty), seam),
out,
);
}
syn::Item::Type(item) if is_public(&item.vis) => {
let seam = item_seam(ItemKind::Type, module, &item.ident);
out.extend(stamp_seam(dyns_in_generics(&item.generics), &seam));
out.extend(stamp_seam(dyns_in_type(&item.ty), &seam));
}
syn::Item::Const(item) if is_public(&item.vis) => {
out.extend(stamp_seam(
dyns_in_type(&item.ty),
&item_seam(ItemKind::Const, module, &item.ident),
));
}
syn::Item::Static(item) if is_public(&item.vis) => {
out.extend(stamp_seam(
dyns_in_type(&item.ty),
&item_seam(ItemKind::Static, module, &item.ident),
));
}
syn::Item::Trait(item) if is_public(&item.vis) => {
let trait_name = strip_raw(&item.ident.to_string());
let trait_seam = item_seam(ItemKind::Trait, module, &item.ident);
out.extend(stamp_seam(dyns_in_generics(&item.generics), &trait_seam));
out.extend(stamp_seam(dyns_in_bounds(&item.supertraits), &trait_seam));
for trait_item in &item.items {
match trait_item {
syn::TraitItem::Fn(method) => {
let seam = trait_method_seam(module, &trait_name, &method.sig.ident);
out.extend(stamp_seam(dyns_in_signature(&method.sig), &seam));
}
syn::TraitItem::Type(assoc) => {
let seam =
trait_assoc_seam(AssocKind::Type, module, &trait_name, &assoc.ident);
out.extend(stamp_seam(dyns_in_bounds(&assoc.bounds), &seam));
out.extend(stamp_seam(dyns_in_generics(&assoc.generics), &seam));
if let Some((_, default)) = &assoc.default {
out.extend(stamp_seam(dyns_in_type(default), &seam));
}
}
syn::TraitItem::Const(assoc) => {
let seam =
trait_assoc_seam(AssocKind::Const, module, &trait_name, &assoc.ident);
out.extend(stamp_seam(dyns_in_type(&assoc.ty), &seam));
}
_ => {}
}
}
}
syn::Item::Impl(item) if item.trait_.is_none() => {
let owner = canonical_self_owner(
&item.self_ty,
uses,
module,
ordinal,
&type_param_names(&item.generics),
);
for (bound, positions) in impl_generics_positions(&item.generics, ordinal) {
let seam = PublicSeam::InherentGenerics {
module: module.to_string(),
owner: owner.clone(),
bound,
};
for position in positions {
let dyns = match position {
GenericsPosition::Bounds(bounds) => dyns_in_bounds(bounds),
GenericsPosition::Type(ty) => dyns_in_type(ty),
};
out.extend(stamp_seam(dyns, &seam));
}
}
for impl_item in &item.items {
match impl_item {
syn::ImplItem::Fn(method) if is_public(&method.vis) => {
let seam = inherent_method_seam(module, &owner, &method.sig.ident);
out.extend(stamp_seam(dyns_in_signature(&method.sig), &seam));
}
syn::ImplItem::Const(assoc) if is_public(&assoc.vis) => {
let seam =
inherent_assoc_seam(AssocKind::Const, module, &owner, &assoc.ident);
out.extend(stamp_seam(dyns_in_type(&assoc.ty), &seam));
}
syn::ImplItem::Type(assoc) if is_public(&assoc.vis) => {
let seam =
inherent_assoc_seam(AssocKind::Type, module, &owner, &assoc.ident);
out.extend(stamp_seam(dyns_in_type(&assoc.ty), &seam));
}
_ => {}
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resolve::{BareFallback, resolve_path_all};
fn resolved(item_src: &str, module: &str) -> Vec<String> {
let item: syn::Item = syn::parse_str(item_src).unwrap();
let uses = UseMap::new();
let mut out = Vec::new();
collect_item_exposures(&item, module, &uses, 0, &mut out);
out.iter()
.flat_map(|e| resolve_path_all(&e.path, &uses, module, BareFallback::Ignore))
.collect()
}
fn exposes(item_src: &str, needle: &str) -> bool {
resolved(item_src, "crate::domain")
.iter()
.any(|p| p == needle)
}
#[test]
fn an_inherent_impl_public_assoc_const_and_type_are_observed() {
assert!(
exposes(
"impl Foo { pub const K: crate::infra::Secret = todo!(); }",
"crate::infra::Secret"
),
"an inherent-impl pub const's type must expose crate::infra::Secret"
);
assert!(
exposes(
"impl Foo { pub type T = crate::infra::Secret; }",
"crate::infra::Secret"
),
"an inherent-impl pub type's target must expose crate::infra::Secret"
);
}
#[test]
fn a_non_public_inherent_assoc_item_is_not_exposed_but_a_pub_method_still_is() {
assert!(
!resolved(
"impl Foo { const K: crate::infra::Secret = todo!(); type T = crate::infra::Secret; }",
"crate::domain"
)
.iter()
.any(|p| p.contains("crate::infra")),
"a non-pub inherent assoc const/type must not be exposed"
);
assert!(
exposes(
"impl Foo { pub fn make() -> crate::infra::Secret { todo!() } }",
"crate::infra::Secret"
),
"a pub inherent method signature is still observed"
);
}
#[test]
fn an_inherent_impl_generic_bound_is_observed() {
assert!(
exposes(
"impl<T: crate::infra::Secret> Foo<T> { pub fn m(&self) {} }",
"crate::infra::Secret"
),
"an inherent-impl generic-param bound must expose crate::infra::Secret"
);
assert!(
exposes(
"impl<T> Foo<T> where T: crate::infra::Secret { pub fn m(&self) {} }",
"crate::infra::Secret"
),
"an inherent-impl where-clause bound must expose crate::infra::Secret"
);
}
#[test]
fn a_supertrait_generic_argument_is_observed() {
assert!(
exposes(
"pub struct S { pub f: Vec<crate::infra::Secret> }",
"crate::infra::Secret"
),
"control: a field generic arg must expose crate::infra::Secret"
);
assert!(
exposes(
"pub trait Facade: AsRef<crate::infra::Secret> {}",
"crate::infra::Secret"
),
"a supertrait bound's generic arg must expose crate::infra::Secret"
);
}
#[test]
fn an_assoc_type_bound_gat_param_and_default_are_observed() {
assert!(
exposes(
"pub trait F { type Bar: Into<crate::infra::Secret>; }",
"crate::infra::Secret"
),
"an associated-type bound's generic arg must be observed"
);
assert!(
exposes(
"pub trait F { type Gat<T: crate::infra::Marker>; }",
"crate::infra::Marker"
),
"a GAT generic-parameter bound must be observed"
);
assert!(
exposes(
"pub trait F { type Bar = crate::infra::Secret; }",
"crate::infra::Secret"
),
"an associated-type default target must be observed"
);
}
#[test]
fn a_forbidden_supertrait_head_still_reacts_and_a_std_bound_does_not() {
assert!(
exposes(
"pub trait Facade: crate::infra::SecretTrait {}",
"crate::infra::SecretTrait"
),
"a forbidden supertrait head must still react"
);
assert!(
!resolved("pub trait Facade: Send + Sync {}", "crate::domain")
.iter()
.any(|p| p.contains("crate::infra")),
"a std supertrait must not expose crate::infra"
);
}
}