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