Skip to main content

rusteal_codegen/
filter.rs

1// Secondary filtering: K2_ dedup, FUNC_Native gate, type exportability, overloads.
2
3use std::collections::{HashMap, HashSet};
4
5use crate::config::Blocklist;
6use crate::context::CodegenContext;
7use crate::schema::*;
8use crate::type_map;
9
10/// Apply all filters to the context's module_classes in place.
11pub fn apply_filters(ctx: &mut CodegenContext, blocklist: &Blocklist) {
12    // Build lookup sets from config blocklist
13    let blocked_classes: HashSet<&str> = blocklist.classes.iter().map(|s| s.as_str()).collect();
14    let blocked_structs: HashSet<&str> = blocklist.structs.iter().map(|s| s.as_str()).collect();
15    let blocked_functions: Vec<(String, String)> = blocklist.function_tuples();
16
17    // Remove blocked classes from both module_classes and ctx.classes
18    for cls in &blocklist.classes {
19        ctx.classes.remove(cls);
20    }
21
22    // The types a generated signature may reference: exactly the ones that will be generated
23    // — in an enabled module and not blocked. Anything else (a class from a module whose
24    // feature is off, a blocklisted class) must make the property/function that references
25    // it drop out, or the generated crate does not compile.
26    let available_types: HashSet<String> = ctx
27        .module_classes
28        .iter()
29        .filter(|(module, _)| ctx.enabled_modules.contains(*module))
30        .flat_map(|(_, classes)| classes.iter().map(|c| c.name.clone()))
31        .filter(|name| !blocked_classes.contains(name.as_str()))
32        .chain(
33            ctx.module_structs
34                .iter()
35                .filter(|(module, _)| ctx.enabled_modules.contains(*module))
36                .flat_map(|(_, structs)| structs.iter().map(|s| s.name.clone()))
37                .filter(|name| !blocked_structs.contains(name.as_str())),
38        )
39        .chain(
40            ctx.module_enums
41                .iter()
42                .filter(|(module, _)| ctx.enabled_modules.contains(*module))
43                .flat_map(|(_, enums)| enums.iter().map(|e| e.name.clone())),
44        )
45        .collect();
46
47    for classes in ctx.module_classes.values_mut() {
48        // Remove blocked classes entirely
49        classes.retain(|c| !blocked_classes.contains(c.name.as_str()));
50
51        for class in classes.iter_mut() {
52            // Filter properties
53            class
54                .props
55                .retain(|p| is_property_exportable(p, &available_types));
56
57            // Filter functions
58            filter_functions(&class.name, &mut class.funcs, &available_types, &blocked_structs, &blocked_functions);
59        }
60    }
61}
62
63/// Check if a property is exportable (supported type, not private/protected, single array dim).
64fn is_property_exportable(prop: &PropertyInfo, available: &HashSet<String>) -> bool {
65    // Skip unsupported types
66    if !type_map::is_supported_type(&prop.prop_type) {
67        return false;
68    }
69
70    // Skip fixed arrays of string/name/text types (CopySingleValue not safe for FString)
71    if prop.array_dim > 1 {
72        match prop.prop_type.as_str() {
73            "StrProperty" | "NameProperty" | "TextProperty" => return false,
74            _ => {} // allow through
75        }
76    }
77
78    // Skip private/protected, unless Blueprint can reach them (AllowPrivateAccess):
79    // they are read through reflection, like any other property.
80    if prop.prop_flags & (CPF_NATIVE_ACCESS_PRIVATE | CPF_NATIVE_ACCESS_PROTECTED) != 0
81        && prop.prop_flags & CPF_BLUEPRINT_VISIBLE == 0
82    {
83        return false;
84    }
85
86    // Delegate properties: validate all params in func_info are exportable
87    if is_delegate_type(&prop.prop_type) {
88        return is_delegate_exportable(prop, available);
89    }
90
91    // Check referenced types are available
92    if let Some(ref cls) = prop.class_name
93        && !available.contains(cls)
94    {
95        return false;
96    }
97    if let Some(ref sn) = prop.struct_name
98        && !available.contains(sn)
99    {
100        return false;
101    }
102    if let Some(ref en) = prop.enum_name
103        && !available.contains(en)
104    {
105        return false;
106    }
107    if let Some(ref iface) = prop.interface_name
108        && !available.contains(iface)
109    {
110        return false;
111    }
112    if prop.prop_type == "ClassProperty"
113        && let Some(ref meta_cls) = prop.meta_class_name
114            && !available.contains(meta_cls)
115    {
116            return false;
117    }
118
119    true
120}
121
122fn is_delegate_type(prop_type: &str) -> bool {
123    matches!(
124        prop_type,
125        "DelegateProperty" | "MulticastInlineDelegateProperty" | "MulticastSparseDelegateProperty"
126    )
127}
128
129/// Check if a delegate property's func_info params are all exportable.
130fn is_delegate_exportable(prop: &PropertyInfo, available: &HashSet<String>) -> bool {
131    let func_info = match &prop.func_info {
132        Some(fi) => fi,
133        None => return false, // No signature info — can't export
134    };
135
136    // Parse func_info params
137    let params = match func_info.get("params").and_then(|p| p.as_array()) {
138        Some(params) => params,
139        None => return true, // No params — zero-arg delegate, always exportable
140    };
141
142    for param_value in params {
143        let param_type = match param_value.get("type").and_then(|t| t.as_str()) {
144            Some(t) => t,
145            None => return false,
146        };
147
148        // Each delegate param type must be supported
149        if !type_map::is_supported_type(param_type) {
150            return false;
151        }
152
153        // Delegate params cannot themselves be delegates or containers
154        if is_delegate_type(param_type) {
155            return false;
156        }
157        if matches!(param_type, "ArrayProperty" | "MapProperty" | "SetProperty") {
158            return false;
159        }
160
161        // Check referenced types are available
162        if let Some(cls) = param_value.get("class_name").and_then(|v| v.as_str())
163            && !available.contains(cls)
164        {
165            return false;
166        }
167        if let Some(sn) = param_value.get("struct_name").and_then(|v| v.as_str())
168            && !available.contains(sn)
169        {
170            return false;
171        }
172        if let Some(en) = param_value.get("enum_name").and_then(|v| v.as_str())
173            && !available.contains(en)
174        {
175            return false;
176        }
177    }
178
179    true
180}
181
182/// Check if a container parameter's inner types are exportable.
183fn is_container_param_exportable(param: &ParamInfo, available: &HashSet<String>) -> bool {
184    match param.prop_type.as_str() {
185        "ArrayProperty" => {
186            if let Some(ref inner) = param.inner_prop {
187                is_inner_type_exportable(inner, available)
188            } else {
189                false
190            }
191        }
192        "MapProperty" => {
193            let key_ok = param.key_prop.as_ref()
194                .map(|k| is_inner_type_exportable(k, available))
195                .unwrap_or(false);
196            let val_ok = param.value_prop.as_ref()
197                .map(|v| is_inner_type_exportable(v, available))
198                .unwrap_or(false);
199            key_ok && val_ok
200        }
201        "SetProperty" => {
202            if let Some(ref elem) = param.element_prop {
203                is_inner_type_exportable(elem, available)
204            } else {
205                false
206            }
207        }
208        _ => false,
209    }
210}
211
212/// Check if a container inner type is supported and its referenced types are available.
213fn is_inner_type_exportable(inner: &PropertyInfo, available: &HashSet<String>) -> bool {
214    if !type_map::is_supported_type(&inner.prop_type) {
215        return false;
216    }
217    // Nested containers not supported
218    if matches!(inner.prop_type.as_str(), "ArrayProperty" | "MapProperty" | "SetProperty") {
219        return false;
220    }
221    if let Some(ref cls) = inner.class_name
222        && !available.contains(cls)
223    {
224        return false;
225    }
226    if let Some(ref sn) = inner.struct_name
227        && !available.contains(sn)
228    {
229        return false;
230    }
231    if let Some(ref en) = inner.enum_name
232        && !available.contains(en)
233    {
234        return false;
235    }
236    if let Some(ref iface) = inner.interface_name
237        && !available.contains(iface)
238    {
239        return false;
240    }
241    true
242}
243
244/// Filter functions on a class: FUNC_Native gate, K2_ dedup, param type check, overload rename.
245fn filter_functions(
246    class_name: &str,
247    funcs: &mut Vec<FunctionInfo>,
248    available: &HashSet<String>,
249    blocked_structs: &HashSet<&str>,
250    blocked_functions: &[(String, String)],
251) {
252    // Step 1: Collect all function names for K2_ dedup
253    let all_names: HashSet<String> = funcs.iter().map(|f| f.name.clone()).collect();
254
255    // Step 2: Filter
256    funcs.retain(|f| {
257        // Function-level blocklist (unlinked symbols)
258        if blocked_functions.iter().any(|(c, func)| c == class_name && func == &f.name) {
259            return false;
260        }
261
262        // FUNC_Native gate
263        if f.func_flags & FUNC_NATIVE == 0 {
264            return false;
265        }
266
267        // K2_ dedup: if this is K2_Foo and Foo also exists, skip K2_Foo
268        if f.name.starts_with("K2_") {
269            let base_name = &f.name[3..];
270            if all_names.contains(base_name) {
271                return false;
272            }
273        }
274
275        // Check all param types are supported and referenced types are available
276        for param in &f.params {
277            if !type_map::is_supported_type(&param.prop_type) {
278                return false;
279            }
280            // Delegate-typed params are not valid in function signatures
281            if is_delegate_type(&param.prop_type) {
282                return false;
283            }
284            // Check container inner types are resolvable
285            if matches!(param.prop_type.as_str(), "ArrayProperty" | "MapProperty" | "SetProperty")
286                && !is_container_param_exportable(param, available)
287            {
288                return false;
289            }
290            if let Some(ref cls) = param.class_name
291                && !available.contains(cls)
292            {
293                return false;
294            }
295            if let Some(ref sn) = param.struct_name
296                && (!available.contains(sn) || blocked_structs.contains(sn.as_str()))
297            {
298                return false;
299            }
300            if let Some(ref en) = param.enum_name
301                && !available.contains(en)
302            {
303                return false;
304            }
305            if let Some(ref iface) = param.interface_name
306                && !available.contains(iface)
307            {
308                return false;
309            }
310            if param.prop_type == "ClassProperty"
311                && let Some(ref meta_cls) = param.meta_class_name
312                    && !available.contains(meta_cls)
313            {
314                    return false;
315            }
316        }
317
318        true
319    });
320
321    // Preserve original UE function names before overload renaming
322    for f in funcs.iter_mut() {
323        f.ue_name = f.name.clone();
324    }
325
326    // Step 3: Handle overloads — rename duplicates with _1, _2 suffix
327    let mut name_counts: HashMap<String, usize> = HashMap::new();
328    for f in funcs.iter() {
329        *name_counts.entry(f.name.clone()).or_default() += 1;
330    }
331
332    let mut name_indices: HashMap<String, usize> = HashMap::new();
333    for f in funcs.iter_mut() {
334        if let Some(&count) = name_counts.get(&f.name)
335            && count > 1
336        {
337            let idx = name_indices.entry(f.name.clone()).or_insert(0);
338            *idx += 1;
339            f.name = format!("{}_{}", f.name, idx);
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use rusteal_ue_flags::{CPF_BLUEPRINT_READ_ONLY, CPF_BLUEPRINT_VISIBLE};
348
349    fn object_prop(name: &str, flags: u64) -> PropertyInfo {
350        serde_json::from_value(serde_json::json!({
351            "name": name,
352            "type": "ObjectProperty",
353            "prop_flags": flags,
354            "class_name": "CharacterMovementComponent",
355        }))
356        .unwrap()
357    }
358
359    #[test]
360    fn private_properties_only_when_blueprint_can_reach_them() {
361        let available: HashSet<String> = ["CharacterMovementComponent".to_string()].into();
362        let read_only = CPF_BLUEPRINT_VISIBLE | CPF_BLUEPRINT_READ_ONLY;
363
364        assert!(is_property_exportable(&object_prop("Public", 0), &available));
365        assert!(!is_property_exportable(&object_prop("Hidden", CPF_NATIVE_ACCESS_PRIVATE), &available));
366        assert!(!is_property_exportable(&object_prop("Guarded", CPF_NATIVE_ACCESS_PROTECTED), &available));
367        assert!(is_property_exportable(
368            &object_prop("CharacterMovement", CPF_NATIVE_ACCESS_PRIVATE | read_only),
369            &available
370        ));
371        assert!(is_property_exportable(
372            &object_prop("Guarded", CPF_NATIVE_ACCESS_PROTECTED | CPF_BLUEPRINT_VISIBLE),
373            &available
374        ));
375    }
376}