svelte-compiler 0.1.4

Core compiler API for the Rust Svelte toolchain
Documentation
use super::*;
use crate::ast::modern::{Root, Script};
use oxc_ast::ast::{
    BindingPattern, Expression as OxcExpression, ImportDeclarationSpecifier, Program, Statement,
};
use oxc_ast_visit::{Visit, walk};

pub(crate) fn infer_runes_mode(options: &CompileOptions, root: &Root) -> bool {
    is_runes_mode(options, root)
}

pub(crate) fn is_runes_mode(options: &CompileOptions, root: &Root) -> bool {
    options.runes.unwrap_or_else(|| {
        root.options
            .as_ref()
            .and_then(|options| options.runes)
            .unwrap_or_else(|| {
                scripts_have_rune_calls(root, has_ambiguous_top_level_state_call(root))
            })
    })
}

fn scripts_have_rune_calls(root: &Root, has_ambiguous_state_call: bool) -> bool {
    [root.module.as_ref(), root.instance.as_ref()]
        .into_iter()
        .flatten()
        .any(|script| script_has_rune_calls(script, has_ambiguous_state_call))
}

fn script_has_rune_calls(script: &Script, has_ambiguous_state_call: bool) -> bool {
    struct RuneCallVisitor {
        has_ambiguous_state_call: bool,
        found: bool,
    }

    impl<'a> Visit<'a> for RuneCallVisitor {
        fn visit_call_expression(&mut self, it: &oxc_ast::ast::CallExpression<'a>) {
            if self.found {
                return;
            }

            if let Some(name) = callee_rune_name(&it.callee)
                && is_rune_mode_trigger(name.as_str())
            {
                if name == "$state" && self.has_ambiguous_state_call {
                    walk::walk_call_expression(self, it);
                    return;
                }
                self.found = true;
                return;
            }

            walk::walk_call_expression(self, it);
        }
    }

    let mut visitor = RuneCallVisitor {
        has_ambiguous_state_call,
        found: false,
    };
    visitor.visit_program(script.oxc_program());
    visitor.found
}

fn callee_rune_name(callee: &OxcExpression<'_>) -> Option<String> {
    match callee.get_inner_expression() {
        OxcExpression::Identifier(reference) => Some(reference.name.to_string()),
        OxcExpression::StaticMemberExpression(member) => {
            let object = member.object.get_inner_expression();
            let OxcExpression::Identifier(object) = object else {
                return None;
            };
            Some(format!("{}.{}", object.name, member.property.name))
        }
        _ => None,
    }
}

fn is_rune_mode_trigger(name: &str) -> bool {
    matches!(
        name,
        "$state"
            | "$state.raw"
            | "$state.snapshot"
            | "$derived"
            | "$derived.by"
            | "$effect"
            | "$effect.active"
            | "$effect.pre"
            | "$effect.tracking"
            | "$effect.root"
            | "$bindable"
            | "$props"
            | "$props.id"
            | "$inspect"
            | "$inspect.trace"
            | "$host"
    )
}

fn has_ambiguous_top_level_state_call(root: &Root) -> bool {
    for script in [root.module.as_ref(), root.instance.as_ref()] {
        let Some(script) = script else {
            continue;
        };
        if script_has_top_level_state_expression(script.oxc_program())
            && script_has_top_level_binding_named(script.oxc_program(), "state")
        {
            return true;
        }
    }
    false
}

fn script_has_top_level_state_expression(program: &Program<'_>) -> bool {
    program.body.iter().any(|statement| {
        let Statement::ExpressionStatement(statement) = statement else {
            return false;
        };
        let OxcExpression::CallExpression(call) = statement.expression.get_inner_expression()
        else {
            return false;
        };
        matches!(
            call.callee.get_inner_expression(),
            OxcExpression::Identifier(identifier) if identifier.name == "$state"
        )
    })
}

fn script_has_top_level_binding_named(program: &Program<'_>, expected: &str) -> bool {
    program
        .body
        .iter()
        .any(|statement| statement_binds_name(statement, expected))
}

fn statement_binds_name(statement: &Statement<'_>, expected: &str) -> bool {
    match statement {
        Statement::VariableDeclaration(declaration) => declaration
            .declarations
            .iter()
            .any(|declarator| binding_pattern_identifier_name(&declarator.id) == Some(expected)),
        Statement::FunctionDeclaration(declaration) => declaration
            .id
            .as_ref()
            .is_some_and(|id| id.name == expected),
        Statement::ClassDeclaration(declaration) => declaration
            .id
            .as_ref()
            .is_some_and(|id| id.name == expected),
        Statement::ImportDeclaration(declaration) => {
            declaration.specifiers.as_ref().is_some_and(|specifiers| {
                specifiers
                    .iter()
                    .any(|specifier| import_specifier_local_name(specifier) == Some(expected))
            })
        }
        _ => false,
    }
}

fn import_specifier_local_name<'a>(
    specifier: &'a ImportDeclarationSpecifier<'a>,
) -> Option<&'a str> {
    match specifier {
        ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
            Some(specifier.local.name.as_str())
        }
        ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
            Some(specifier.local.name.as_str())
        }
        ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
            Some(specifier.local.name.as_str())
        }
    }
}

fn binding_pattern_identifier_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
    match pattern.get_binding_identifier() {
        Some(identifier) => Some(identifier.name.as_str()),
        None => None,
    }
}

#[cfg(test)]
mod tests {
    use super::infer_runes_mode;
    use crate::api::CompileOptions;
    use crate::compiler::phases::parse::parse_component_for_compile;

    fn runes_mode(source: &str) -> bool {
        let parsed = parse_component_for_compile(source).expect("parse component");
        infer_runes_mode(&CompileOptions::default(), parsed.root())
    }

    #[test]
    fn runes_mode_uses_svelte_options_true_from_ast() {
        assert!(runes_mode("<svelte:options runes />"));
    }

    #[test]
    fn runes_mode_uses_svelte_options_false_from_ast() {
        assert!(!runes_mode("<svelte:options runes={false} />"));
    }

    #[test]
    fn runes_mode_uses_rune_calls_when_options_are_absent() {
        assert!(runes_mode("<script>let count = $state(0);</script>"));
    }

    #[test]
    fn runes_mode_ignores_ambiguous_top_level_state_calls() {
        assert!(!runes_mode("<script>let state = 1; $state(0);</script>"));
    }
}