1use proc_macro::TokenStream;
9use quote::quote;
10use syn::{parse_macro_input, DeriveInput, ItemFn};
11use syn::visit_mut::{self, VisitMut};
12use syn::spanned::Spanned;
13
14#[proc_macro_derive(BoundaryKind, attributes(boundary_kind))]
27pub fn derive_boundary_kind(input: TokenStream) -> TokenStream {
28 let input = parse_macro_input!(input as DeriveInput);
29 let name = &input.ident;
30 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
31
32 let crate_path = input.attrs.iter()
34 .find(|a| a.path().is_ident("boundary_kind"))
35 .and_then(|a| {
36 let mut path = None;
37 let _ = a.parse_nested_meta(|meta| {
38 if meta.path.is_ident("crate_path") {
39 let value = meta.value()?;
40 let lit: syn::LitStr = value.parse()?;
41 path = Some(lit.value());
42 }
43 Ok(())
44 });
45 path
46 });
47
48 let trait_path: syn::Path = match crate_path.as_deref() {
49 Some("crate") => syn::parse_quote!(crate::BoundaryKind),
50 Some(p) => syn::parse_str(&format!("{p}::BoundaryKind")).unwrap(),
51 None => syn::parse_quote!(origin::BoundaryKind),
52 };
53
54 let expanded = quote! {
55 impl #impl_generics #trait_path for #name #ty_generics #where_clause {}
56 };
57
58 TokenStream::from(expanded)
59}
60
61#[proc_macro_attribute]
77pub fn boundary_check(_attr: TokenStream, item: TokenStream) -> TokenStream {
78 let mut func = parse_macro_input!(item as ItemFn);
79
80 let mut checker = BoundaryChecker;
81 checker.visit_item_fn_mut(&mut func);
82
83 TokenStream::from(quote! { #func })
84}
85
86struct BoundaryChecker;
88
89impl VisitMut for BoundaryChecker {
90 fn visit_expr_method_call_mut(&mut self, node: &mut syn::ExprMethodCall) {
91 visit_mut::visit_expr_method_call_mut(self, node);
93
94 if (node.method == "unwrap" && node.args.is_empty())
95 || (node.method == "expect" && node.args.len() == 1)
96 {
97 let span = node.method.span();
98 let error = syn::parse_quote_spanned! { span =>
99 compile_error!(
100 "boundary not handled: `.unwrap()` bypasses boundary checking\n\
101 \n\
102 A Value<T> may be at a boundary. Handle it explicitly:\n\
103 \n\
104 \x20 match value {\n\
105 \x20 Value::Contents(v) => /* safe to use */,\n\
106 \x20 Value::Boundary { reason, last } => /* crossed edge, has context */,\n\
107 \x20 Value::Origin(reason) => /* absolute boundary, no value */,\n\
108 \x20 }\n\
109 \n\
110 or use `.or(fallback)` to provide a default value"
111 )
112 };
113 *node.receiver = error;
114 }
115 }
116
117 fn visit_expr_match_mut(&mut self, node: &mut syn::ExprMatch) {
118 visit_mut::visit_expr_match_mut(self, node);
120
121 for arm in &mut node.arms {
124 if is_wildcard_boundary_arm(&arm.pat) {
125 let span = arm.pat.span();
126 let error: syn::Expr = syn::parse_quote_spanned! { span =>
127 compile_error!(
128 "boundary kind ignored: wildcard `Boundary { .. }` catches all boundary kinds\n\
129 \n\
130 Each boundary kind exists for a reason. Handle them individually:\n\
131 \n\
132 \x20 Value::Contents(v) => /* safe to use */,\n\
133 \x20 Value::Boundary { reason: Kind::LowConfidence { .. }, last } => /* refer */,\n\
134 \x20 Value::Boundary { reason: Kind::Hallucinated { .. }, .. } => /* refuse */,\n\
135 \x20 Value::Origin(reason) => /* absolute boundary, no value */,\n\
136 \n\
137 A wildcard boundary arm is the new `.unwrap()` — it acknowledges\n\
138 the boundary exists and then ignores what it means"
139 )
140 };
141 arm.body = Box::new(error);
142 }
143 }
144 }
145}
146
147use syn::Pat;
148
149fn is_wildcard_boundary_arm(pat: &Pat) -> bool {
157 match pat {
158 Pat::Struct(ps) => {
160 let path_str = path_to_string(&ps.path);
161 if !path_str.ends_with("Boundary") {
162 return false;
163 }
164 let reason_field = ps.fields.iter().find(|f| {
167 f.member == syn::Member::Named(syn::Ident::new("reason", proc_macro2::Span::call_site()))
168 });
169 match reason_field {
170 None => true, Some(f) => matches!(*f.pat, Pat::Wild(_)), }
173 }
174 Pat::Wild(_) => true,
176 Pat::Ident(pi) => pi.subpat.is_none(),
178 _ => false,
179 }
180}
181
182fn path_to_string(path: &syn::Path) -> String {
183 path.segments.iter()
184 .map(|s| s.ident.to_string())
185 .collect::<Vec<_>>()
186 .join("::")
187}