crabsoup_mlua_analyze/
lib.rs

1extern crate crabsoup_luau_sys;
2
3use std::{
4    ffi::{c_char, c_uint, c_void},
5    mem,
6};
7
8#[repr(C)]
9struct RustString {
10    data: *const c_char,
11    len: usize,
12}
13impl RustString {
14    unsafe fn decode(&self) -> String {
15        let slice =
16            std::str::from_utf8(std::slice::from_raw_parts(self.data as *const _, self.len))
17                .expect("Received invalid UTF-8 from Luau?");
18        slice.to_string()
19    }
20    unsafe fn encode(str: &str) -> Self {
21        RustString { data: str.as_ptr() as *const _, len: str.len() }
22    }
23}
24
25#[repr(C)]
26struct RustLineColumn {
27    line: c_uint,
28    column: c_uint,
29}
30
31#[derive(Debug)]
32struct RustCheckResultReceiver {
33    results: Vec<AnalyzeResult>,
34}
35
36#[derive(Clone, Debug)]
37pub struct AnalyzeResult {
38    pub location: String,
39    pub location_start: AnalyzeLocation,
40    pub location_end: AnalyzeLocation,
41    pub is_error: bool,
42    pub is_lint: bool,
43    pub message: String,
44}
45
46#[derive(Copy, Clone, Debug)]
47pub struct AnalyzeLocation {
48    pub line: usize,
49    pub column: usize,
50}
51impl From<RustLineColumn> for AnalyzeLocation {
52    fn from(value: RustLineColumn) -> Self {
53        AnalyzeLocation { line: value.line as usize, column: value.column as usize }
54    }
55}
56
57#[repr(transparent)]
58struct FrontendWrapper(c_void);
59
60extern "C-unwind" {
61    fn luauAnalyze_new_frontend() -> *mut FrontendWrapper;
62    fn luauAnalyze_register_definitions(
63        wrapper: *mut FrontendWrapper,
64        module_name: RustString,
65        definitions: RustString,
66    ) -> bool;
67    fn luauAnalyze_set_deprecation(
68        wrapper: *mut FrontendWrapper,
69        module_path: RustString,
70        replacement: RustString,
71    ) -> bool;
72    fn luauAnalyze_freeze_definitions(wrapper: *mut FrontendWrapper);
73    #[allow(improper_ctypes)] // only used as an opaque reference
74    fn luauAnalyze_check(
75        receiver: *mut RustCheckResultReceiver,
76        wrapper: *mut FrontendWrapper,
77        name: RustString,
78        contents: RustString,
79        is_module: bool,
80    );
81    fn luauAnalyze_free_frontend(wrapper: *mut FrontendWrapper);
82}
83
84mod exports {
85    use super::*;
86
87    #[no_mangle]
88    pub unsafe extern "C-unwind" fn luauAnalyze_push_result(
89        receiver: *mut RustCheckResultReceiver,
90        module: RustString,
91        error_start: RustLineColumn,
92        error_end: RustLineColumn,
93        is_error: bool,
94        is_lint: bool,
95        message: RustString,
96    ) {
97        (*receiver).results.push(AnalyzeResult {
98            location: module.decode(),
99            location_start: error_start.into(),
100            location_end: error_end.into(),
101            is_error,
102            is_lint,
103            message: message.decode(),
104        });
105    }
106}
107
108pub struct LuaAnalyzerBuilder {
109    underlying: *mut FrontendWrapper,
110}
111impl LuaAnalyzerBuilder {
112    pub fn new() -> Self {
113        LuaAnalyzerBuilder { underlying: unsafe { luauAnalyze_new_frontend() } }
114    }
115
116    pub fn add_definitions(&mut self, name: &str, definitions: &str) {
117        if !unsafe {
118            luauAnalyze_register_definitions(
119                self.underlying,
120                RustString::encode(name),
121                RustString::encode(definitions),
122            )
123        } {
124            panic!("Failed to parse definitions file: {name}");
125        }
126    }
127
128    pub fn set_deprecation(&mut self, name: &str, replacement: Option<&str>) {
129        let replacement = replacement.unwrap_or("");
130        if !unsafe {
131            luauAnalyze_set_deprecation(
132                self.underlying,
133                RustString::encode(name),
134                RustString::encode(replacement),
135            )
136        } {
137            panic!("set_deprecation failed");
138        }
139    }
140
141    pub fn build(self) -> LuaAnalyzer {
142        unsafe { luauAnalyze_freeze_definitions(self.underlying) }
143        let out = LuaAnalyzer { underlying: self.underlying };
144        mem::forget(self);
145        out
146    }
147}
148impl Drop for LuaAnalyzerBuilder {
149    fn drop(&mut self) {
150        unsafe { luauAnalyze_free_frontend(self.underlying) }
151    }
152}
153
154pub struct LuaAnalyzer {
155    underlying: *mut FrontendWrapper,
156}
157impl LuaAnalyzer {
158    pub fn check(&self, name: &str, contents: &str, is_module: bool) -> Vec<AnalyzeResult> {
159        let mut receiver = RustCheckResultReceiver { results: vec![] };
160        unsafe {
161            luauAnalyze_check(
162                &mut receiver,
163                self.underlying,
164                RustString::encode(name),
165                RustString::encode(contents),
166                is_module,
167            );
168        }
169        receiver.results
170    }
171}
172impl Drop for LuaAnalyzer {
173    fn drop(&mut self) {
174        unsafe { luauAnalyze_free_frontend(self.underlying) }
175    }
176}