use syn::visit::Visit;
use crate::finding::{
PathExposure, SemanticFinding, field_seam, fn_seam, inherent_assoc_seam, inherent_method_seam,
item_seam, member_label, render_sig_tail, tag_paths, trait_assoc_seam, trait_method_seam,
};
use crate::resolve::{
DynCollector, ImplTraitCollector, PathCollector, ShapeExposure, UseMap, canonical_self_owner,
path_to_string, stamp_seam, strip_raw, type_to_string,
};
use crate::syn_util::is_public;
pub(crate) fn collect_item_return_impl_traits(
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(impl_traits_in_return(&item.sig), &seam));
}
syn::Item::Trait(item) if is_public(&item.vis) => {
let trait_name = strip_raw(&item.ident.to_string());
for trait_item in &item.items {
if let syn::TraitItem::Fn(method) = trait_item {
let seam = trait_method_seam(module, &trait_name, &method.sig.ident);
out.extend(stamp_seam(impl_traits_in_return(&method.sig), &seam));
}
}
}
syn::Item::Impl(item) if item.trait_.is_none() => {
let owner = canonical_self_owner(&item.self_ty, uses, module, ordinal);
for impl_item in &item.items {
if let syn::ImplItem::Fn(method) = impl_item {
if is_public(&method.vis) {
let seam = inherent_method_seam(&owner, &method.sig.ident);
out.extend(stamp_seam(impl_traits_in_return(&method.sig), &seam));
}
}
}
}
_ => {}
}
}
fn impl_traits_in_return(sig: &syn::Signature) -> Vec<ShapeExposure> {
let mut collector = ImplTraitCollector::default();
if let syn::ReturnType::Type(_, ty) = &sig.output {
collector.visit_type(ty);
}
collector.exposures
}
pub(crate) fn collect_item_async_exposures(
item: &syn::Item,
module: &str,
uses: &UseMap,
ordinal: usize,
out: &mut Vec<String>,
) {
match item {
syn::Item::Fn(item) if is_public(&item.vis) => {
if item.sig.asyncness.is_some() {
out.push(
SemanticFinding::AsyncFreeFn {
module: module.to_string(),
name: strip_raw(&item.sig.ident.to_string()),
tail: render_sig_tail(&item.sig),
}
.to_string(),
);
}
}
syn::Item::Trait(item) if is_public(&item.vis) => {
let trait_name = strip_raw(&item.ident.to_string());
for trait_item in &item.items {
if let syn::TraitItem::Fn(method) = trait_item {
if method.sig.asyncness.is_some() {
out.push(
SemanticFinding::AsyncTraitMethod {
module: module.to_string(),
trait_name: trait_name.clone(),
name: strip_raw(&method.sig.ident.to_string()),
tail: render_sig_tail(&method.sig),
}
.to_string(),
);
}
}
}
}
syn::Item::Impl(item) if item.trait_.is_none() => {
let owner = canonical_self_owner(&item.self_ty, uses, module, ordinal);
for impl_item in &item.items {
if let syn::ImplItem::Fn(method) = impl_item {
if is_public(&method.vis) && method.sig.asyncness.is_some() {
out.push(
SemanticFinding::AsyncInherentMethod {
owner: owner.clone(),
name: strip_raw(&method.sig.ident.to_string()),
tail: render_sig_tail(&method.sig),
}
.to_string(),
);
}
}
}
}
_ => {}
}
}
fn paths_in_signature(sig: &syn::Signature) -> Vec<syn::Path> {
let mut c = PathCollector::default();
c.visit_signature(sig);
c.paths
}
fn paths_in_type(ty: &syn::Type) -> Vec<syn::Path> {
let mut c = PathCollector::default();
c.visit_type(ty);
c.paths
}
fn paths_in_generics(generics: &syn::Generics) -> Vec<syn::Path> {
let mut c = PathCollector::default();
c.visit_generics(generics);
c.paths
}
fn dyns_in_signature(sig: &syn::Signature) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
c.visit_signature(sig);
c.exposures
}
fn dyns_in_type(ty: &syn::Type) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
c.visit_type(ty);
c.exposures
}
fn dyns_in_generics(generics: &syn::Generics) -> Vec<ShapeExposure> {
let mut c = DynCollector::default();
c.visit_generics(generics);
c.exposures
}
pub(crate) fn collect_item_exposures(
item: &syn::Item,
module: &str,
uses: &UseMap,
ordinal: usize,
out: &mut Vec<PathExposure>,
) {
match item {
syn::Item::Fn(item) if is_public(&item.vis) => {
let seam = fn_seam(module, &item.sig.ident);
out.extend(tag_paths(paths_in_signature(&item.sig), &seam));
}
syn::Item::Struct(item) if is_public(&item.vis) => {
let name = strip_raw(&item.ident.to_string());
out.extend(tag_paths(
paths_in_generics(&item.generics),
&item_seam("struct", module, &item.ident),
));
for (index, field) in item.fields.iter().enumerate() {
if is_public(&field.vis) {
let seam = field_seam("field", module, &name, &member_label(index, field));
out.extend(tag_paths(paths_in_type(&field.ty), &seam));
}
}
}
syn::Item::Enum(item) if is_public(&item.vis) => {
let name = strip_raw(&item.ident.to_string());
out.extend(tag_paths(
paths_in_generics(&item.generics),
&item_seam("enum", module, &item.ident),
));
for variant in &item.variants {
let owner = format!("{name}::{}", strip_raw(&variant.ident.to_string()));
for (index, field) in variant.fields.iter().enumerate() {
let seam = field_seam("variant", module, &owner, &member_label(index, field));
out.extend(tag_paths(paths_in_type(&field.ty), &seam));
}
}
}
syn::Item::Union(item) if is_public(&item.vis) => {
let name = strip_raw(&item.ident.to_string());
out.extend(tag_paths(
paths_in_generics(&item.generics),
&item_seam("union", module, &item.ident),
));
for (index, field) in item.fields.named.iter().enumerate() {
if is_public(&field.vis) {
let seam = field_seam("field", module, &name, &member_label(index, field));
out.extend(tag_paths(paths_in_type(&field.ty), &seam));
}
}
}
syn::Item::Type(item) if is_public(&item.vis) => {
let seam = item_seam("type", module, &item.ident);
out.extend(tag_paths(paths_in_generics(&item.generics), &seam));
out.extend(tag_paths(paths_in_type(&item.ty), &seam));
}
syn::Item::Const(item) if is_public(&item.vis) => {
out.extend(tag_paths(
paths_in_type(&item.ty),
&item_seam("const", module, &item.ident),
));
}
syn::Item::Static(item) if is_public(&item.vis) => {
out.extend(tag_paths(
paths_in_type(&item.ty),
&item_seam("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("trait", module, &item.ident);
out.extend(tag_paths(paths_in_generics(&item.generics), &trait_seam));
out.extend(tag_paths(paths_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(tag_paths(paths_in_signature(&method.sig), &seam));
}
syn::TraitItem::Type(assoc) => {
let seam = trait_assoc_seam("type", module, &trait_name, &assoc.ident);
out.extend(tag_paths(paths_in_bounds(&assoc.bounds), &seam));
out.extend(tag_paths(paths_in_generics(&assoc.generics), &seam));
if let Some((_, ty)) = &assoc.default {
out.extend(tag_paths(paths_in_type(ty), &seam));
}
}
syn::TraitItem::Const(assoc) => {
let seam = trait_assoc_seam("const", module, &trait_name, &assoc.ident);
out.extend(tag_paths(paths_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);
for impl_item in &item.items {
match impl_item {
syn::ImplItem::Fn(method) if is_public(&method.vis) => {
let seam = inherent_method_seam(&owner, &method.sig.ident);
out.extend(tag_paths(paths_in_signature(&method.sig), &seam));
}
syn::ImplItem::Const(assoc) if is_public(&assoc.vis) => {
let seam = inherent_assoc_seam("const", &owner, &assoc.ident);
out.extend(tag_paths(paths_in_type(&assoc.ty), &seam));
}
syn::ImplItem::Type(assoc) if is_public(&assoc.vis) => {
let seam = inherent_assoc_seam("type", &owner, &assoc.ident);
out.extend(tag_paths(paths_in_type(&assoc.ty), &seam));
}
_ => {}
}
}
}
syn::Item::Use(item) if is_public(&item.vis) => {
walk_reexport_tree(
&item.tree,
Vec::new(),
module,
item.leading_colon.is_some(),
out,
);
}
syn::Item::ExternCrate(item) if is_public(&item.vis) && item.ident != "self" => {
let name = strip_raw(&item.ident.to_string());
out.push(PathExposure {
seam: format!("pub extern crate {name}"),
path: syn::Path::from(item.ident.clone()),
is_reexport: true,
});
}
_ => {}
}
}
fn is_self_segment(ident: &syn::Ident) -> bool {
ident == "self"
}
fn walk_reexport_tree(
tree: &syn::UseTree,
prefix: Vec<syn::Ident>,
module: &str,
leading_colon: bool,
out: &mut Vec<PathExposure>,
) {
match tree {
syn::UseTree::Path(path) => {
let mut segs = prefix;
segs.push(path.ident.clone());
walk_reexport_tree(&path.tree, segs, module, leading_colon, out);
}
syn::UseTree::Name(name) => {
if is_self_segment(&name.ident) {
let exported = prefix.last().map(seg_name);
push_reexport(&prefix, exported.as_deref(), module, leading_colon, out);
} else {
let exported = seg_name(&name.ident);
let mut segs = prefix;
segs.push(name.ident.clone());
push_reexport(&segs, Some(&exported), module, leading_colon, out);
}
}
syn::UseTree::Rename(rename) => {
let alias = seg_name(&rename.rename);
if alias == "_" {
return; }
if is_self_segment(&rename.ident) {
push_reexport(&prefix, Some(&alias), module, leading_colon, out);
} else {
let mut segs = prefix;
segs.push(rename.ident.clone());
push_reexport(&segs, Some(&alias), module, leading_colon, out);
}
}
syn::UseTree::Glob(_) => {
push_reexport(&prefix, Some("*"), module, leading_colon, out);
}
syn::UseTree::Group(group) => {
for item in &group.items {
walk_reexport_tree(item, prefix.clone(), module, leading_colon, out);
}
}
}
}
fn seg_name(ident: &syn::Ident) -> String {
strip_raw(&ident.to_string())
}
fn push_reexport(
segs: &[syn::Ident],
exported: Option<&str>,
module: &str,
leading_colon: bool,
out: &mut Vec<PathExposure>,
) {
let (Some(exported), false) = (exported, segs.is_empty()) else {
return;
};
let segments = segs
.iter()
.map(|ident| syn::PathSegment {
ident: ident.clone(),
arguments: syn::PathArguments::None,
})
.collect();
out.push(PathExposure {
path: syn::Path {
leading_colon: leading_colon.then(<syn::Token![::]>::default),
segments,
},
seam: format!("pub use {module}::{exported}"),
is_reexport: true,
});
}
fn paths_in_return(sig: &syn::Signature) -> Vec<syn::Path> {
let mut c = PathCollector::default();
if let syn::ReturnType::Type(_, ty) = &sig.output {
c.visit_type(ty);
}
c.paths
}
fn paths_in_bounds(
bounds: &syn::punctuated::Punctuated<syn::TypeParamBound, syn::token::Plus>,
) -> Vec<syn::Path> {
let mut c = PathCollector::default();
for bound in bounds {
c.visit_type_param_bound(bound);
}
c.paths
}
pub(crate) fn collect_trait_impl_exposures(
item: &syn::Item,
module: &str,
uses: &UseMap,
ordinal: usize,
out: &mut Vec<PathExposure>,
) {
let syn::Item::Impl(item) = item else { return };
let Some((_, trait_path, _)) = &item.trait_ else {
return; };
let trait_label = path_to_string(trait_path).unwrap_or_else(|| format!("trait_#{ordinal}"));
let self_label = canonical_self_owner(&item.self_ty, uses, module, ordinal);
let prefix = format!("impl {trait_label} for {self_label}");
if let Some(syn::PathArguments::AngleBracketed(args)) =
trait_path.segments.last().map(|s| &s.arguments)
{
let seam = format!("{prefix} (trait-arg)");
for arg in &args.args {
match arg {
syn::GenericArgument::Type(ty) => out.extend(tag_paths(paths_in_type(ty), &seam)),
syn::GenericArgument::AssocType(at) => {
out.extend(tag_paths(paths_in_type(&at.ty), &seam))
}
_ => {}
}
}
}
out.extend(tag_paths(
paths_in_type(&item.self_ty),
&format!("{prefix} (self)"),
));
for param in &item.generics.params {
match param {
syn::GenericParam::Type(tp) => {
let key = strip_raw(&tp.ident.to_string());
let seam = format!("{prefix} (where {key})");
out.extend(tag_paths(paths_in_bounds(&tp.bounds), &seam));
}
syn::GenericParam::Const(cp) => {
let key = strip_raw(&cp.ident.to_string());
let seam = format!("{prefix} (where {key})");
out.extend(tag_paths(paths_in_type(&cp.ty), &seam));
}
syn::GenericParam::Lifetime(_) => {}
}
}
if let Some(where_clause) = &item.generics.where_clause {
for predicate in &where_clause.predicates {
if let syn::WherePredicate::Type(pt) = predicate {
let key = type_to_string(&pt.bounded_ty).unwrap_or_else(|| "_".to_string());
let seam = format!("{prefix} (where {key})");
out.extend(tag_paths(paths_in_type(&pt.bounded_ty), &seam));
out.extend(tag_paths(paths_in_bounds(&pt.bounds), &seam));
}
}
}
for impl_item in &item.items {
match impl_item {
syn::ImplItem::Type(assoc) => {
let seam = format!("{prefix} (assoc {})", strip_raw(&assoc.ident.to_string()));
out.extend(tag_paths(paths_in_type(&assoc.ty), &seam));
}
syn::ImplItem::Const(assoc) => {
let seam = format!("{prefix} (assoc {})", strip_raw(&assoc.ident.to_string()));
out.extend(tag_paths(paths_in_type(&assoc.ty), &seam));
}
syn::ImplItem::Fn(method) => {
let seam = format!(
"{prefix} (method {} return)",
strip_raw(&method.sig.ident.to_string())
);
out.extend(tag_paths(paths_in_return(&method.sig), &seam));
}
_ => {}
}
}
}
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("struct", module, &item.ident),
));
for (index, field) in item.fields.iter().enumerate() {
if is_public(&field.vis) {
let seam = field_seam("field", module, &name, &member_label(index, field));
out.extend(stamp_seam(dyns_in_type(&field.ty), &seam));
}
}
}
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("enum", module, &item.ident),
));
for variant in &item.variants {
let owner = format!("{name}::{}", strip_raw(&variant.ident.to_string()));
for (index, field) in variant.fields.iter().enumerate() {
let seam = field_seam("variant", module, &owner, &member_label(index, field));
out.extend(stamp_seam(dyns_in_type(&field.ty), &seam));
}
}
}
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("union", module, &item.ident),
));
for (index, field) in item.fields.named.iter().enumerate() {
if is_public(&field.vis) {
let seam = field_seam("field", module, &name, &member_label(index, field));
out.extend(stamp_seam(dyns_in_type(&field.ty), &seam));
}
}
}
syn::Item::Type(item) if is_public(&item.vis) => {
let seam = item_seam("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("const", module, &item.ident),
));
}
syn::Item::Static(item) if is_public(&item.vis) => {
out.extend(stamp_seam(
dyns_in_type(&item.ty),
&item_seam("static", module, &item.ident),
));
}
syn::Item::Trait(item) if is_public(&item.vis) => {
let trait_name = strip_raw(&item.ident.to_string());
out.extend(stamp_seam(
dyns_in_generics(&item.generics),
&item_seam("trait", module, &item.ident),
));
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) => {
if let Some((_, default)) = &assoc.default {
let seam = trait_assoc_seam("type", module, &trait_name, &assoc.ident);
out.extend(stamp_seam(dyns_in_type(default), &seam));
}
}
syn::TraitItem::Const(assoc) => {
let seam = trait_assoc_seam("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);
for impl_item in &item.items {
match impl_item {
syn::ImplItem::Fn(method) if is_public(&method.vis) => {
let seam = inherent_method_seam(&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("const", &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("type", &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};
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()
.filter_map(|e| resolve_path(&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 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"
);
}
}