wowlab_tidy/languages/rust/rules/interop/
native_escape_hatches.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::ast::{self, HasName};
4use wowlab_types::sim::FastMap;
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "wrapper with all escape hatches",
12 code: "pub struct Handle(*const u8);\nimpl Handle {\n pub unsafe fn from_native(raw: *const u8) -> Self { Self(raw) }\n pub fn into_native(self) -> *const u8 { self.0 }\n pub fn to_native(&self) -> *const u8 { self.0 }\n}",
13 pass: true,
14 },
15 Example {
16 label: "wrapper without any escape hatch",
17 code: "pub struct Handle(*mut u8);",
18 pass: false,
19 },
20 Example {
21 label: "from_native not unsafe",
22 code: "pub struct Handle(*const u8);\nimpl Handle {\n pub fn from_native(raw: *const u8) -> Self { Self(raw) }\n pub fn into_native(self) -> *const u8 { self.0 }\n pub fn to_native(&self) -> *const u8 { self.0 }\n}",
23 pass: false,
24 },
25 Example {
26 label: "missing to_native",
27 code: "pub struct Handle(*const u8);\nimpl Handle {\n pub unsafe fn from_native(raw: *const u8) -> Self { Self(raw) }\n pub fn into_native(self) -> *const u8 { self.0 }\n}",
28 pass: false,
29 },
30 Example {
31 label: "private wrapper",
32 code: "struct Handle(*const u8);",
33 pass: true,
34 },
35 Example {
36 label: "pointer plus length is not a plain wrapper",
37 code: "pub struct Slice {\n data: *const u8,\n len: usize,\n}",
38 pass: true,
39 },
40 Example {
41 label: "non-pointer field",
42 code: "pub struct Id(u64);",
43 pass: true,
44 },
45 Example {
46 label: "wrapper in test module",
47 code: "#[cfg(test)]\nmod tests {\n pub struct Handle(*const u8);\n}",
48 pass: true,
49 },
50];
51
52crate::ast_rule!(
53 native_escape_hatches,
54 "Require `unsafe fn from_native`, `into_native`, and `to_native` on public raw-pointer wrapper structs.",
55 "Interop users need unsafe escape hatches to construct wrappers from native handles obtained elsewhere and to pass wrapped handles back over FFI (M-ESCAPE-HATCHES).",
56 Low,
57);
58
59fn check_native_escape_hatches(ctx: &AstCtx<'_>) -> Vec<Violation> {
60 let methods = collect_methods(ctx);
61
62 let wrappers = ctx
63 .nodes::<ast::Struct>()
64 .filter(|item| !ctx.is_in_test(item))
65 .filter(|item| super::support::is_fully_public(item) && is_raw_pointer_wrapper(item));
66
67 wrappers
68 .flat_map(|item| check_struct(ctx, &item, &methods))
69 .collect()
70}
71
72fn collect_methods(ctx: &AstCtx<'_>) -> FastMap<String, Vec<(String, bool)>> {
74 let mut methods: FastMap<String, Vec<(String, bool)>> = FastMap::default();
75
76 for item_impl in ctx
77 .nodes::<ast::Impl>()
78 .filter(|item| item.trait_().is_none())
79 {
80 let Some(name) = item_impl.self_ty().and_then(super::support::type_name) else {
81 continue;
82 };
83 let Some(items) = item_impl.assoc_item_list() else {
84 continue;
85 };
86
87 methods
88 .entry(name)
89 .or_default()
90 .extend(items.assoc_items().filter_map(|item| match item {
91 ast::AssocItem::Fn(function) => Some((
92 function.name()?.text().to_string(),
93 function.unsafe_token().is_some(),
94 )),
95 _ => None,
96 }));
97 }
98
99 methods
100}
101
102fn is_raw_pointer_wrapper(item: &ast::Struct) -> bool {
103 item.field_list().is_some_and(|fields| {
104 let types = super::support::field_types(fields);
105
106 types.len() == 1 && matches!(types.first(), Some(ast::Type::PtrType(_)))
107 })
108}
109
110fn check_struct(
111 ctx: &AstCtx<'_>,
112 item: &ast::Struct,
113 methods: &FastMap<String, Vec<(String, bool)>>,
114) -> Vec<Violation> {
115 let Some(name) = super::support::name_text(item) else {
116 return Vec::new();
117 };
118 let item_methods = methods.get(&name);
119 let has = |target: &str| item_methods.is_some_and(|m| m.iter().any(|(n, _)| n == target));
120 let from_unsafe = item_methods.is_some_and(|m| {
121 m.iter()
122 .any(|(n, is_unsafe)| n == "from_native" && *is_unsafe)
123 });
124 let mut violations = Vec::new();
125
126 if !has("from_native") {
127 violations.push(ctx.violation(
128 item,
129 format!(
130 "raw-pointer wrapper `{name}` is missing an `unsafe fn from_native` escape hatch"
131 ),
132 ));
133 } else if !from_unsafe {
134 violations.push(ctx.violation(
135 item,
136 format!("`{name}::from_native` must be an `unsafe fn` — constructing from a foreign native handle has safety requirements"),
137 ));
138 }
139
140 if !has("into_native") {
141 violations.push(ctx.violation(
142 item,
143 format!("raw-pointer wrapper `{name}` is missing an `into_native` escape hatch"),
144 ));
145 }
146
147 if !has("to_native") {
148 violations.push(ctx.violation(
149 item,
150 format!("raw-pointer wrapper `{name}` is missing a `to_native` escape hatch"),
151 ));
152 }
153
154 violations
155}
156
157crate::tidy_ast_test!(check_native_escape_hatches, {
158 crate::example_tests!(EXAMPLES, check_native_escape_hatches);
159
160 #[gtest]
161 fn bare_wrapper_reports_all_three_methods() -> Result<()> {
162 let v = run("pub struct Handle(*mut u8);");
163 verify_eq!(v.len(), 3)?;
164
165 Ok(())
166 }
167});