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    // The types whose header no other module can include (an Internal or
48    // Private one): a function of theirs, or one taking them, has no C++
49    // wrapper the plugin can compile. Their properties are read through
50    // reflection, which needs no header.
51    let unreachable_headers: HashSet<String> = ctx
52        .module_classes
53        .values()
54        .flat_map(|classes| classes.iter().filter(|c| !c.header_public).map(|c| c.name.clone()))
55        .chain(
56            ctx.module_structs
57                .values()
58                .flat_map(|structs| structs.iter().filter(|s| !s.header_public).map(|s| s.name.clone())),
59        )
60        .collect();
61
62    for classes in ctx.module_classes.values_mut() {
63        // Remove blocked classes entirely
64        classes.retain(|c| !blocked_classes.contains(c.name.as_str()));
65
66        for class in classes.iter_mut() {
67            // Filter properties
68            class
69                .props
70                .retain(|p| is_property_exportable(p, &available_types));
71
72            // Filter functions
73            if !class.header_public {
74                class.funcs.clear();
75            }
76            class.funcs.retain(|f| !references_any(f, &unreachable_headers));
77            filter_functions(&class.name, &mut class.funcs, &available_types, &blocked_structs, &blocked_functions);
78        }
79    }
80}
81
82/// Whether a function's parameters name any of `types`, directly or as a
83/// container's element, key or value.
84fn references_any(func: &FunctionInfo, types: &HashSet<String>) -> bool {
85    let names = |p: &PropertyInfo| {
86        [p.class_name.clone(), p.meta_class_name.clone(), p.struct_name.clone()]
87            .into_iter()
88            .flatten()
89            .collect::<Vec<_>>()
90    };
91    func.params.iter().any(|param| {
92        let direct = [param.class_name.as_ref(), param.meta_class_name.as_ref(), param.struct_name.as_ref()];
93        let inner = [
94            param.inner_prop.as_deref(),
95            param.key_prop.as_deref(),
96            param.value_prop.as_deref(),
97            param.element_prop.as_deref(),
98        ];
99        direct.into_iter().flatten().any(|name| types.contains(name))
100            || inner.into_iter().flatten().flat_map(names).any(|name| types.contains(&name))
101    })
102}
103
104/// Check if a property is exportable (supported type, not private/protected, single array dim).
105fn is_property_exportable(prop: &PropertyInfo, available: &HashSet<String>) -> bool {
106    // Skip unsupported types
107    if !type_map::is_supported_type(&prop.prop_type) {
108        return false;
109    }
110
111    // Skip fixed arrays of string/name/text types (CopySingleValue not safe for FString)
112    if prop.array_dim > 1 {
113        match prop.prop_type.as_str() {
114            "StrProperty" | "NameProperty" | "TextProperty" => return false,
115            _ => {} // allow through
116        }
117    }
118
119    // Skip private/protected, unless Blueprint can reach them (AllowPrivateAccess,
120    // or a Blueprint-assignable delegate such as UEnvQueryInstanceBlueprintWrapper's
121    // OnQueryFinishedEvent) or a child class's defaults can set them
122    // (AController's bAttachToPawn, editable): they are read through
123    // reflection, like any other property.
124    if prop.prop_flags & (CPF_NATIVE_ACCESS_PRIVATE | CPF_NATIVE_ACCESS_PROTECTED) != 0
125        && prop.prop_flags & (CPF_BLUEPRINT_VISIBLE | CPF_EDIT | CPF_BLUEPRINT_ASSIGNABLE) == 0
126    {
127        return false;
128    }
129
130    // Delegate properties: validate all params in func_info are exportable
131    if is_delegate_type(&prop.prop_type) {
132        return is_delegate_exportable(prop, available);
133    }
134
135    // Check referenced types are available
136    if let Some(ref cls) = prop.class_name
137        && !available.contains(cls)
138    {
139        return false;
140    }
141    if let Some(ref sn) = prop.struct_name
142        && !available.contains(sn)
143    {
144        return false;
145    }
146    if let Some(ref en) = prop.enum_name
147        && !available.contains(en)
148    {
149        return false;
150    }
151    if let Some(ref iface) = prop.interface_name
152        && !available.contains(iface)
153    {
154        return false;
155    }
156    if prop.prop_type == "ClassProperty"
157        && let Some(ref meta_cls) = prop.meta_class_name
158            && !available.contains(meta_cls)
159    {
160            return false;
161    }
162
163    true
164}
165
166fn is_delegate_type(prop_type: &str) -> bool {
167    matches!(
168        prop_type,
169        "DelegateProperty" | "MulticastInlineDelegateProperty" | "MulticastSparseDelegateProperty"
170    )
171}
172
173/// Check if a delegate property's func_info params are all exportable.
174fn is_delegate_exportable(prop: &PropertyInfo, available: &HashSet<String>) -> bool {
175    let func_info = match &prop.func_info {
176        Some(fi) => fi,
177        None => return false, // No signature info — can't export
178    };
179
180    // Parse func_info params
181    let params = match func_info.get("params").and_then(|p| p.as_array()) {
182        Some(params) => params,
183        None => return true, // No params — zero-arg delegate, always exportable
184    };
185
186    for param_value in params {
187        let param_type = match param_value.get("type").and_then(|t| t.as_str()) {
188            Some(t) => t,
189            None => return false,
190        };
191
192        // Each delegate param type must be supported
193        if !type_map::is_supported_type(param_type) {
194            return false;
195        }
196
197        // Delegate params cannot themselves be delegates or containers
198        if is_delegate_type(param_type) {
199            return false;
200        }
201        if matches!(param_type, "ArrayProperty" | "MapProperty" | "SetProperty") {
202            return false;
203        }
204
205        // Check referenced types are available
206        if let Some(cls) = param_value.get("class_name").and_then(|v| v.as_str())
207            && !available.contains(cls)
208        {
209            return false;
210        }
211        if let Some(sn) = param_value.get("struct_name").and_then(|v| v.as_str())
212            && !available.contains(sn)
213        {
214            return false;
215        }
216        if let Some(en) = param_value.get("enum_name").and_then(|v| v.as_str())
217            && !available.contains(en)
218        {
219            return false;
220        }
221    }
222
223    true
224}
225
226/// Check if a container parameter's inner types are exportable.
227fn is_container_param_exportable(param: &ParamInfo, available: &HashSet<String>) -> bool {
228    match param.prop_type.as_str() {
229        "ArrayProperty" => {
230            if let Some(ref inner) = param.inner_prop {
231                is_inner_type_exportable(inner, available)
232            } else {
233                false
234            }
235        }
236        "MapProperty" => {
237            let key_ok = param.key_prop.as_ref()
238                .map(|k| is_inner_type_exportable(k, available))
239                .unwrap_or(false);
240            let val_ok = param.value_prop.as_ref()
241                .map(|v| is_inner_type_exportable(v, available))
242                .unwrap_or(false);
243            key_ok && val_ok
244        }
245        "SetProperty" => {
246            if let Some(ref elem) = param.element_prop {
247                is_inner_type_exportable(elem, available)
248            } else {
249                false
250            }
251        }
252        _ => false,
253    }
254}
255
256/// Check if a container inner type is supported and its referenced types are available.
257fn is_inner_type_exportable(inner: &PropertyInfo, available: &HashSet<String>) -> bool {
258    if !type_map::is_supported_type(&inner.prop_type) {
259        return false;
260    }
261    // Nested containers not supported
262    if matches!(inner.prop_type.as_str(), "ArrayProperty" | "MapProperty" | "SetProperty") {
263        return false;
264    }
265    if let Some(ref cls) = inner.class_name
266        && !available.contains(cls)
267    {
268        return false;
269    }
270    if let Some(ref sn) = inner.struct_name
271        && !available.contains(sn)
272    {
273        return false;
274    }
275    if let Some(ref en) = inner.enum_name
276        && !available.contains(en)
277    {
278        return false;
279    }
280    if let Some(ref iface) = inner.interface_name
281        && !available.contains(iface)
282    {
283        return false;
284    }
285    true
286}
287
288/// Filter functions on a class: FUNC_Native gate, K2_ dedup, param type check, overload rename.
289fn filter_functions(
290    class_name: &str,
291    funcs: &mut Vec<FunctionInfo>,
292    available: &HashSet<String>,
293    blocked_structs: &HashSet<&str>,
294    blocked_functions: &[(String, String)],
295) {
296    // Step 1: Collect all function names for K2_ dedup
297    let all_names: HashSet<String> = funcs.iter().map(|f| f.name.clone()).collect();
298
299    // Step 2: Filter
300    funcs.retain(|f| {
301        // Function-level blocklist (unlinked symbols)
302        if blocked_functions.iter().any(|(c, func)| c == class_name && func == &f.name) {
303            return false;
304        }
305
306        // FUNC_Native gate
307        if f.func_flags & FUNC_NATIVE == 0 {
308            return false;
309        }
310
311        // K2_ dedup: if this is K2_Foo and Foo also exists, skip K2_Foo
312        if f.name.starts_with("K2_") {
313            let base_name = &f.name[3..];
314            if all_names.contains(base_name) {
315                return false;
316            }
317        }
318
319        // Check all param types are supported and referenced types are available
320        for param in &f.params {
321            if !type_map::is_supported_type(&param.prop_type) {
322                return false;
323            }
324            // Delegate-typed params are not valid in function signatures
325            if is_delegate_type(&param.prop_type) {
326                return false;
327            }
328            // Check container inner types are resolvable
329            if matches!(param.prop_type.as_str(), "ArrayProperty" | "MapProperty" | "SetProperty")
330                && !is_container_param_exportable(param, available)
331            {
332                return false;
333            }
334            if let Some(ref cls) = param.class_name
335                && !available.contains(cls)
336            {
337                return false;
338            }
339            if let Some(ref sn) = param.struct_name
340                && (!available.contains(sn) || blocked_structs.contains(sn.as_str()))
341            {
342                return false;
343            }
344            if let Some(ref en) = param.enum_name
345                && !available.contains(en)
346            {
347                return false;
348            }
349            if let Some(ref iface) = param.interface_name
350                && !available.contains(iface)
351            {
352                return false;
353            }
354            if param.prop_type == "ClassProperty"
355                && let Some(ref meta_cls) = param.meta_class_name
356                    && !available.contains(meta_cls)
357            {
358                    return false;
359            }
360        }
361
362        true
363    });
364
365    // Preserve original UE function names before overload renaming
366    for f in funcs.iter_mut() {
367        f.ue_name = f.name.clone();
368    }
369
370    // Step 3: Handle overloads — rename duplicates with _1, _2 suffix
371    let mut name_counts: HashMap<String, usize> = HashMap::new();
372    for f in funcs.iter() {
373        *name_counts.entry(f.name.clone()).or_default() += 1;
374    }
375
376    let mut name_indices: HashMap<String, usize> = HashMap::new();
377    for f in funcs.iter_mut() {
378        if let Some(&count) = name_counts.get(&f.name)
379            && count > 1
380        {
381            let idx = name_indices.entry(f.name.clone()).or_insert(0);
382            *idx += 1;
383            f.name = format!("{}_{}", f.name, idx);
384        }
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use rusteal_ue_flags::{CPF_BLUEPRINT_READ_ONLY, CPF_BLUEPRINT_VISIBLE};
392
393    fn object_prop(name: &str, flags: u64) -> PropertyInfo {
394        serde_json::from_value(serde_json::json!({
395            "name": name,
396            "type": "ObjectProperty",
397            "prop_flags": flags,
398            "class_name": "CharacterMovementComponent",
399        }))
400        .unwrap()
401    }
402
403    #[test]
404    fn private_properties_only_when_blueprint_can_reach_them() {
405        let available: HashSet<String> = ["CharacterMovementComponent".to_string()].into();
406        let read_only = CPF_BLUEPRINT_VISIBLE | CPF_BLUEPRINT_READ_ONLY;
407
408        assert!(is_property_exportable(&object_prop("Public", 0), &available));
409        assert!(!is_property_exportable(&object_prop("Hidden", CPF_NATIVE_ACCESS_PRIVATE), &available));
410        assert!(!is_property_exportable(&object_prop("Guarded", CPF_NATIVE_ACCESS_PROTECTED), &available));
411        assert!(is_property_exportable(
412            &object_prop("CharacterMovement", CPF_NATIVE_ACCESS_PRIVATE | read_only),
413            &available
414        ));
415        assert!(is_property_exportable(
416            &object_prop("Guarded", CPF_NATIVE_ACCESS_PROTECTED | CPF_BLUEPRINT_VISIBLE),
417            &available
418        ));
419        assert!(is_property_exportable(
420            &object_prop("Editable", CPF_NATIVE_ACCESS_PROTECTED | CPF_EDIT),
421            &available
422        ));
423        assert!(is_property_exportable(
424            &object_prop("Assignable", CPF_NATIVE_ACCESS_PROTECTED | CPF_BLUEPRINT_ASSIGNABLE),
425            &available
426        ));
427    }
428
429    fn function(params: serde_json::Value) -> FunctionInfo {
430        serde_json::from_value(serde_json::json!({
431            "name": "Probe",
432            "func_flags": FUNC_NATIVE,
433            "params": params,
434        }))
435        .unwrap()
436    }
437
438    fn param(fields: serde_json::Value) -> serde_json::Value {
439        let mut param = serde_json::json!({
440            "name": "P", "type": "ObjectProperty", "prop_flags": 0,
441            "enum_name": null, "enum_cpp_name": null, "enum_cpp_form": null,
442            "enum_underlying_type": null, "class_name": null, "meta_class_name": null,
443            "struct_name": null, "interface_name": null, "func_info": null,
444            "inner_prop": null, "key_prop": null, "value_prop": null,
445            "element_prop": null, "default": null,
446        });
447        for (key, value) in fields.as_object().unwrap() {
448            param[key] = value.clone();
449        }
450        param
451    }
452
453    #[test]
454    fn functions_naming_unreachable_headers_drop_out() {
455        let internal: HashSet<String> = ["NiagaraDataInterfaceArrayMesh".to_string()].into();
456        let takes = |fields| function(serde_json::json!([param(fields)]));
457
458        assert!(references_any(&takes(serde_json::json!({"class_name": "NiagaraDataInterfaceArrayMesh"})), &internal));
459        assert!(references_any(
460            &takes(serde_json::json!({
461                "type": "ArrayProperty",
462                "inner_prop": {"name": "P", "type": "ObjectProperty", "prop_flags": 0,
463                               "class_name": "NiagaraDataInterfaceArrayMesh"},
464            })),
465            &internal
466        ));
467        assert!(!references_any(&takes(serde_json::json!({"class_name": "NiagaraComponent"})), &internal));
468        assert!(!references_any(&function(serde_json::json!([])), &internal));
469    }
470}