reluxscript 0.1.4

Write AST transformations once. Compile to Babel, SWC, and beyond.
Documentation
/// Kitchen Sink Plugin - Tests major ReluxScript plugin features
/// This is a comprehensive test of the language for AST transformation plugins

plugin KitchenSinkPlugin {

    // === Type Definitions ===

    struct HookInfo {
        name: Str,
        hook_type: Str,
        args_count: Number,
    }

    struct ComponentStats {
        name: Str,
        hooks: Vec<HookInfo>,
        has_jsx: bool,
    }

    struct MemberInfo {
        object: Str,
        property: Str,
    }

    // === Plugin State ===

    struct State {
        components: Vec<ComponentStats>,
        current_component: Option<Str>,
        removed_count: i32,
        visited_nodes: HashSet<Str>,
    }

    // === Helper Functions ===

    fn is_component_name(name: &Str) -> bool {
        if name.is_empty() {
            return false;
        }
        let first_char = name.chars().next().unwrap();
        first_char.is_uppercase()
    }

    fn is_hook_call(name: &Str) -> bool {
        name.starts_with("use") && name.len() > 3
    }

    fn categorize_hook(name: &Str) -> Str {
        if name == "useState" || name == "useReducer" {
            return "state".into();
        }
        if name == "useEffect" || name == "useLayoutEffect" {
            return "effect".into();
        }
        if name == "useRef" {
            return "ref".into();
        }
        if name == "useMemo" || name == "useCallback" {
            return "memo".into();
        }
        return format!("custom:{}", name);
    }

    fn should_remove_console(method: &Str) -> bool {
        method == "log" || method == "warn" || method == "debug"
    }

    pub fn format_stats(stats: &ComponentStats) -> Str {
        format!("{} has {} hooks", stats.name, stats.hooks.len())
    }

    fn get_callee_name(callee: &Expression) -> Option<Str> {
        if let Expression::Identifier(id) = callee {
            Some(id.name.clone())
        } else if let Expression::MemberExpression(member) = callee {
            Some(member.property.clone())
        } else {
            None
        }
    }

    fn extract_member_call(call: &CallExpression) -> Option<MemberInfo> {
        if let Expression::MemberExpression(member) = &call.callee {
            if let Expression::Identifier(obj) = &member.object {
                return Some(MemberInfo {
                    object: obj.name.clone(),
                    property: member.property.clone(),
                });
            }
        }
        None
    }

    // === Visitor Methods ===

    fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
        let name = node.id.name.clone();

        // Check if this is a React component
        if is_component_name(&name) {
            self.state.current_component = Some(name.clone());

            // Initialize component stats
            let stats = ComponentStats {
                name: name.clone(),
                hooks: vec![],
                has_jsx: false,
            };
            self.state.components.push(stats);
        }

        // Visit children
        node.visit_children(self);

        // Clear component context
        self.state.current_component = None;
    }

    fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
        // Track hooks in components
        if let Some(component_name) = &self.state.current_component {
            if let Some(callee_name) = get_callee_name(&node.callee) {
                if is_hook_call(&callee_name) {
                    let hook_info = HookInfo {
                        name: callee_name.clone(),
                        hook_type: categorize_hook(&callee_name),
                        args_count: 0,
                    };

                    // Find current component and add hook
                    for component in &mut self.state.components {
                        if component.name == *component_name {
                            component.hooks.push(hook_info);
                            break;
                        }
                    }
                }
            }
        }

        // Remove console calls
        if let Some(member) = extract_member_call(node) {
            if member.object == "console" && should_remove_console(&member.property) {
                self.state.removed_count += 1;
            }
        }

        node.visit_children(self);
    }

    fn visit_identifier(node: &mut Identifier, ctx: &Context) {
        let name = node.name.clone();

        // Track visited nodes
        self.state.visited_nodes.insert(name.clone());

        // Rename specific identifiers
        if name == "oldName" {
            *node = Identifier {
                name: "newName",
            };
        }

        // Pattern matching with matches! macro
        if matches!(node.name.as_str(), "foo" | "bar" | "baz") {
            let new_name = format!("renamed_{}", node.name);
            *node = Identifier {
                name: new_name,
            };
        }
    }

    fn visit_jsx_element(node: &mut JSXElement, ctx: &Context) {
        // Track JSX usage in components
        if let Some(component_name) = &self.state.current_component {
            for component in &mut self.state.components {
                if component.name == *component_name {
                    // Create new struct with updated field
                    let updated = ComponentStats {
                        name: component.name.clone(),
                        hooks: component.hooks.clone(),
                        has_jsx: true,
                    };
                    *component = updated;
                    break;
                }
            }
        }

        node.visit_children(self);
    }

    fn visit_variable_declarator(node: &mut VariableDeclarator, ctx: &Context) {
        // Pattern matching with if-let
        if let Some(init) = &node.init {
            if let Expression::ArrayExpression(arr) = init {
                // Destructuring pattern
                let _size = arr.elements.len();
            }
        }

        node.visit_children(self);
    }

    // === Collection Operations ===

    fn collect_hook_names(component: &ComponentStats) -> Vec<Str> {
        component.hooks.iter()
            .map(|h| h.name.clone())
            .collect()
    }

    fn has_hooks(component: &ComponentStats) -> bool {
        component.hooks.len() > 0
    }

    fn count_by_type(component: &ComponentStats, target: &Str) -> i32 {
        let mut count = 0;
        for hook in &component.hooks {
            if hook.hook_type == *target {
                count += 1;
            }
        }
        count
    }

    // === Option and Result Handling ===

    fn get_first_hook(stats: &ComponentStats) -> Option<Str> {
        if stats.hooks.is_empty() {
            None
        } else {
            Some(stats.hooks[0].name.clone())
        }
    }

    fn safe_get_name(stats: &ComponentStats) -> Result<Str, Str> {
        if stats.name.is_empty() {
            Err("No name")
        } else {
            Ok(stats.name.clone())
        }
    }

    fn process_component(stats: &ComponentStats) -> Result<(), Str> {
        let _name = safe_get_name(stats)?;
        let _first_hook = get_first_hook(stats).unwrap_or("none".into());
        Ok(())
    }

    // === Ternary Expression Test ===

    fn get_hook_label(count: i32) -> Str {
        if count > 0 { "hooks" } else { "no hooks" }
    }
}