fluidattacks-blends-domain 0.35.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
use alloc::string::String;

use crate::NodeId;

/// The lexical construct a [`StackScope`] stands for.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[allow(
    clippy::exhaustive_enums,
    reason = "a fixed ontology: the lexical scope kinds the stack graph model distinguishes"
)]
pub enum ScopeType {
    /// A `{}`-delimited block (loop body, conditional branch, ...).
    Block,
    /// A class or struct body.
    Class,
    /// A function/method/variable declaration's own scope.
    Declaration,
    /// A whole source file.
    File,
    /// A function or method body.
    Function,
    /// A namespace or module body.
    Namespace,
}

/// A scope node: where name lookups stop descending further down the stack.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct StackScope {
    /// Whether names defined in this scope are visible from outside it.
    pub is_exported: bool,
    /// The lexical construct this scope stands for, when known.
    pub scope_type: Option<ScopeType>,
}

/// A reference: a symbol pushed onto the stack to be resolved against a definition.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct SymbolPush {
    /// The referenced symbol's name.
    pub symbol: String,
    /// Resolution order among competing pushes; lower wins.
    pub precedence: i64,
    /// Whether this push is a reference (as opposed to an internal relay).
    pub is_reference: bool,
}

/// What makes a [`SymbolPop`] a definition rather than a plain re-export.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct Definition {
    /// Whether the definition is private to its declaring module.
    pub is_private: bool,
    /// The name of the symbol that must be in scope to see this definition, if any.
    pub visibility_guard: Option<String>,
}

/// A definition: a symbol popped off the stack, ending a reference's search.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct SymbolPop {
    /// The defined symbol's name.
    pub symbol: String,
    /// Resolution order among competing pops; lower wins.
    pub precedence: i64,
    /// What this pop defines, when it is a true definition rather than a relay.
    pub definition: Option<Definition>,
}

/// How a [`StackNode::PopScopedSymbol`] binds the module it imports from.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct ImportBinding {
    /// The imported module's own name (the last path segment).
    pub module_stem: String,
    /// The symbol imported from that module, when it names one.
    pub source_symbol: Option<String>,
    /// How many parent directories a relative import climbs.
    pub relative_level: u32,
    /// Whether the whole module (not just one symbol) is bound.
    pub binds_module: bool,
}

/// One node of a file's stack graph: a step in the push/pop path name
/// resolution walks to connect a reference to its definition.
#[derive(Clone, PartialEq, Eq, Debug)]
#[allow(
    clippy::exhaustive_enums,
    reason = "a fixed ontology: the stack-graphs algorithm's node kinds"
)]
pub enum StackNode {
    /// Pops every scope off the path's scope stack.
    DropScopes,
    /// Follows the top of the scope stack without consuming a symbol.
    JumpTo,
    /// Pops a symbol that also opens one of its exported scopes.
    PopScopedSymbol {
        /// The symbol being resolved and what, if anything, it defines.
        pop: SymbolPop,
        /// The import this definition re-exports from, if it is a re-export.
        import: Option<ImportBinding>,
    },
    /// Pops a plain symbol, ending the search if it is a definition.
    PopSymbol(SymbolPop),
    /// Pushes a symbol and opens a scope the path can later jump into.
    PushScopedSymbol {
        /// The symbol being referenced.
        push: SymbolPush,
        /// Id of the scope this reference exposes to later pops.
        scope: NodeId,
    },
    /// Pushes a plain symbol to be resolved against a later pop.
    PushSymbol(SymbolPush),
    /// The path's starting point.
    Root,
    /// A lexical scope a path may enter while searching for a definition.
    Scope(StackScope),
}

impl StackNode {
    /// The symbol this node pushes or pops, for symbol-carrying variants.
    #[must_use]
    pub fn symbol(&self) -> Option<&str> {
        match self {
            Self::PushScopedSymbol { push, .. } | Self::PushSymbol(push) => Some(&push.symbol),
            Self::PopScopedSymbol { pop, .. } | Self::PopSymbol(pop) => Some(&pop.symbol),
            Self::DropScopes | Self::JumpTo | Self::Root | Self::Scope(_) => None,
        }
    }

    /// This node's resolution precedence; 0 for variants without one.
    #[must_use]
    pub const fn precedence(&self) -> i64 {
        match self {
            Self::PushScopedSymbol { push, .. } | Self::PushSymbol(push) => push.precedence,
            Self::PopScopedSymbol { pop, .. } | Self::PopSymbol(pop) => pop.precedence,
            Self::DropScopes | Self::JumpTo | Self::Root | Self::Scope(_) => 0,
        }
    }

    /// Whether this node is a reference push (as opposed to an internal relay).
    #[must_use]
    pub const fn is_reference(&self) -> bool {
        match self {
            Self::PushScopedSymbol { push, .. } | Self::PushSymbol(push) => push.is_reference,
            Self::DropScopes
            | Self::JumpTo
            | Self::PopScopedSymbol { .. }
            | Self::PopSymbol(_)
            | Self::Root
            | Self::Scope(_) => false,
        }
    }

    /// Whether this node pops a true definition (as opposed to a relay or a reference).
    #[must_use]
    pub const fn is_definition(&self) -> bool {
        match self {
            Self::PopScopedSymbol { pop, .. } | Self::PopSymbol(pop) => pop.definition.is_some(),
            Self::DropScopes
            | Self::JumpTo
            | Self::PushScopedSymbol { .. }
            | Self::PushSymbol(_)
            | Self::Root
            | Self::Scope(_) => false,
        }
    }

    /// What this definition re-exports from, when it is a re-export.
    #[must_use]
    pub const fn import_binding(&self) -> Option<&ImportBinding> {
        match self {
            Self::PopScopedSymbol { import, .. } => import.as_ref(),
            Self::DropScopes
            | Self::JumpTo
            | Self::PopSymbol(_)
            | Self::PushScopedSymbol { .. }
            | Self::PushSymbol(_)
            | Self::Root
            | Self::Scope(_) => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Definition, ImportBinding, ScopeType, StackNode, StackScope, SymbolPop, SymbolPush,
    };
    use crate::NodeId;
    use alloc::borrow::ToOwned;

    fn push(symbol: &str) -> SymbolPush {
        SymbolPush {
            symbol: symbol.to_owned(),
            precedence: 0,
            is_reference: true,
        }
    }

    fn pop(symbol: &str) -> SymbolPop {
        SymbolPop {
            symbol: symbol.to_owned(),
            precedence: 1,
            definition: Some(Definition::default()),
        }
    }

    fn scoped_pop(symbol: &str) -> StackNode {
        StackNode::PopScopedSymbol {
            pop: pop(symbol),
            import: None,
        }
    }

    fn file_scope() -> StackNode {
        StackNode::Scope(StackScope {
            is_exported: true,
            scope_type: Some(ScopeType::File),
        })
    }

    #[test]
    fn symbol_nodes_carry_their_symbol() {
        let scoped_push = StackNode::PushScopedSymbol {
            push: push("send"),
            scope: NodeId(4),
        };

        assert_eq!(StackNode::PushSymbol(push("send")).symbol(), Some("send"));
        assert_eq!(scoped_push.symbol(), Some("send"));
        assert_eq!(StackNode::PopSymbol(pop("utils")).symbol(), Some("utils"));
        assert_eq!(scoped_pop("send").symbol(), Some("send"));
        assert_eq!(StackNode::Root.symbol(), None);
        assert_eq!(file_scope().symbol(), None);
    }

    #[test]
    fn nodes_without_a_symbol_take_no_precedence() {
        assert_eq!(scoped_pop("send").precedence(), 1);
        assert_eq!(StackNode::PushSymbol(push("send")).precedence(), 0);
        assert_eq!(StackNode::JumpTo.precedence(), 0);
        assert_eq!(file_scope().precedence(), 0);
    }

    #[test]
    fn only_marked_push_nodes_are_references() {
        let chain_link = SymbolPush {
            is_reference: false,
            ..push("utils")
        };

        assert!(StackNode::PushSymbol(push("send")).is_reference());
        assert!(StackNode::PushScopedSymbol {
            push: push("send"),
            scope: NodeId(4),
        }
        .is_reference());
        assert!(!StackNode::PushSymbol(chain_link).is_reference());
        assert!(!scoped_pop("send").is_reference());
        assert!(!StackNode::DropScopes.is_reference());
    }

    #[test]
    fn only_marked_pop_nodes_are_definitions() {
        let path_segment = SymbolPop {
            definition: None,
            ..pop("utils")
        };

        assert!(StackNode::PopSymbol(pop("send")).is_definition());
        assert!(scoped_pop("send").is_definition());
        assert!(!StackNode::PopSymbol(path_segment).is_definition());
        assert!(!StackNode::PushSymbol(push("send")).is_definition());
        assert!(!file_scope().is_definition());
    }

    #[test]
    fn only_scoped_pops_bind_imports() {
        let binding = ImportBinding {
            module_stem: "utils".to_owned(),
            source_symbol: Some("send".to_owned()),
            relative_level: 1,
            binds_module: false,
        };
        let import = StackNode::PopScopedSymbol {
            pop: pop("send"),
            import: Some(binding.clone()),
        };

        assert_eq!(import.import_binding(), Some(&binding));
        assert!(import.is_definition());
        assert_eq!(scoped_pop("send").import_binding(), None);
        assert_eq!(StackNode::PopSymbol(pop("send")).import_binding(), None);
        assert_eq!(StackNode::PushSymbol(push("send")).import_binding(), None);
    }
}