Skip to main content

wowlab_tidy/languages/rust/rules/interop/
native_escape_hatches.rs

1#[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
72// #t(fn: rust_alloc_in_loop) method names are retained as owned cross-impl lookup keys
73fn 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});