Skip to main content

fluidattacks_blends_domain/stack/
node.rs

1use alloc::string::String;
2
3/// The lexical construct a [`StackScope`] stands for.
4#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
5#[allow(
6    clippy::exhaustive_enums,
7    reason = "a fixed ontology: the lexical scope kinds the stack graph model distinguishes"
8)]
9pub enum ScopeType {
10    /// A `{}`-delimited block (loop body, conditional branch, ...).
11    Block,
12    /// A class or struct body.
13    Class,
14    /// A function/method/variable declaration's own scope.
15    Declaration,
16    /// A whole source file.
17    File,
18    /// A function or method body.
19    Function,
20    /// A namespace or module body.
21    Namespace,
22}
23
24/// A scope node: where name lookups stop descending further down the stack.
25#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
26#[non_exhaustive]
27pub struct StackScope {
28    /// The lexical construct this scope stands for, when known.
29    pub scope_type: Option<ScopeType>,
30}
31
32/// A reference: a symbol pushed onto the stack to be resolved against a definition.
33#[derive(Clone, PartialEq, Eq, Debug)]
34#[non_exhaustive]
35pub struct SymbolPush {
36    /// The referenced symbol's name.
37    pub symbol: String,
38    /// Resolution order among competing pushes; lower wins.
39    pub precedence: i64,
40    /// Whether this push is a reference (as opposed to an internal relay).
41    pub is_reference: bool,
42}
43
44/// What makes a [`SymbolPop`] a definition rather than a plain re-export.
45#[derive(Clone, PartialEq, Eq, Debug, Default)]
46#[non_exhaustive]
47pub struct Definition {
48    /// Whether the definition is private to its declaring module.
49    pub is_private: bool,
50    /// The name of the symbol that must be in scope to see this definition, if any.
51    pub visibility_guard: Option<String>,
52}
53
54/// A definition: a symbol popped off the stack, ending a reference's search.
55#[derive(Clone, PartialEq, Eq, Debug)]
56#[non_exhaustive]
57pub struct SymbolPop {
58    /// The defined symbol's name.
59    pub symbol: String,
60    /// Resolution order among competing pops; lower wins.
61    pub precedence: i64,
62    /// What this pop defines, when it is a true definition rather than a relay.
63    pub definition: Option<Definition>,
64    /// The import this definition binds, when it comes from an import.
65    pub import: Option<ImportBinding>,
66}
67
68/// How an import's [`SymbolPop`] binds the module it imports from.
69#[derive(Clone, PartialEq, Eq, Debug)]
70#[non_exhaustive]
71pub struct ImportBinding {
72    /// The imported module's own name (the last path segment).
73    pub module_stem: String,
74    /// The symbol imported from that module, when it names one.
75    pub source_symbol: Option<String>,
76    /// How many parent directories a relative import climbs.
77    pub relative_level: u32,
78    /// Whether the whole module (not just one symbol) is bound.
79    pub binds_module: bool,
80}
81
82/// One node of a file's stack graph: a step in the push/pop path name
83/// resolution walks to connect a reference to its definition.
84#[derive(Clone, PartialEq, Eq, Debug)]
85#[allow(
86    clippy::exhaustive_enums,
87    reason = "a fixed ontology: the stack-graphs algorithm's node kinds"
88)]
89pub enum StackNode {
90    /// Pops a symbol, ending the search if it is a definition.
91    PopSymbol(SymbolPop),
92    /// Pushes a symbol to be resolved against a later pop.
93    PushSymbol(SymbolPush),
94    /// The path's starting point.
95    Root,
96    /// A lexical scope a path may enter while searching for a definition.
97    Scope(StackScope),
98}
99
100impl StackNode {
101    /// The symbol this node pushes or pops, for symbol-carrying variants.
102    #[must_use]
103    pub fn symbol(&self) -> Option<&str> {
104        match self {
105            Self::PushSymbol(push) => Some(&push.symbol),
106            Self::PopSymbol(pop) => Some(&pop.symbol),
107            Self::Root | Self::Scope(_) => None,
108        }
109    }
110
111    /// This node's resolution precedence; 0 for variants without one.
112    #[must_use]
113    pub const fn precedence(&self) -> i64 {
114        match self {
115            Self::PushSymbol(push) => push.precedence,
116            Self::PopSymbol(pop) => pop.precedence,
117            Self::Root | Self::Scope(_) => 0,
118        }
119    }
120
121    /// Whether this node is a reference push (as opposed to an internal relay).
122    #[must_use]
123    pub const fn is_reference(&self) -> bool {
124        match self {
125            Self::PushSymbol(push) => push.is_reference,
126            Self::PopSymbol(_) | Self::Root | Self::Scope(_) => false,
127        }
128    }
129
130    /// Whether this node pops a true definition (as opposed to a relay or a reference).
131    #[must_use]
132    pub const fn is_definition(&self) -> bool {
133        match self {
134            Self::PopSymbol(pop) => pop.definition.is_some(),
135            Self::PushSymbol(_) | Self::Root | Self::Scope(_) => false,
136        }
137    }
138
139    /// The import this definition binds, when it comes from an import.
140    #[must_use]
141    pub const fn import_binding(&self) -> Option<&ImportBinding> {
142        match self {
143            Self::PopSymbol(pop) => pop.import.as_ref(),
144            Self::PushSymbol(_) | Self::Root | Self::Scope(_) => None,
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::{
152        Definition, ImportBinding, ScopeType, StackNode, StackScope, SymbolPop, SymbolPush,
153    };
154    use alloc::borrow::ToOwned;
155
156    fn push(symbol: &str) -> SymbolPush {
157        SymbolPush {
158            symbol: symbol.to_owned(),
159            precedence: 0,
160            is_reference: true,
161        }
162    }
163
164    fn pop(symbol: &str) -> SymbolPop {
165        SymbolPop {
166            symbol: symbol.to_owned(),
167            precedence: 1,
168            definition: Some(Definition::default()),
169            import: None,
170        }
171    }
172
173    fn file_scope() -> StackNode {
174        StackNode::Scope(StackScope {
175            scope_type: Some(ScopeType::File),
176        })
177    }
178
179    #[test]
180    fn symbol_nodes_carry_their_symbol() {
181        assert_eq!(StackNode::PushSymbol(push("send")).symbol(), Some("send"));
182        assert_eq!(StackNode::PopSymbol(pop("utils")).symbol(), Some("utils"));
183        assert_eq!(StackNode::Root.symbol(), None);
184        assert_eq!(file_scope().symbol(), None);
185    }
186
187    #[test]
188    fn nodes_without_a_symbol_take_no_precedence() {
189        assert_eq!(StackNode::PopSymbol(pop("send")).precedence(), 1);
190        assert_eq!(StackNode::PushSymbol(push("send")).precedence(), 0);
191        assert_eq!(StackNode::Root.precedence(), 0);
192        assert_eq!(file_scope().precedence(), 0);
193    }
194
195    #[test]
196    fn only_marked_push_nodes_are_references() {
197        let chain_link = SymbolPush {
198            is_reference: false,
199            ..push("utils")
200        };
201
202        assert!(StackNode::PushSymbol(push("send")).is_reference());
203        assert!(!StackNode::PushSymbol(chain_link).is_reference());
204        assert!(!StackNode::PopSymbol(pop("send")).is_reference());
205        assert!(!StackNode::Root.is_reference());
206    }
207
208    #[test]
209    fn only_marked_pop_nodes_are_definitions() {
210        let path_segment = SymbolPop {
211            definition: None,
212            ..pop("utils")
213        };
214
215        assert!(StackNode::PopSymbol(pop("send")).is_definition());
216        assert!(!StackNode::PopSymbol(path_segment).is_definition());
217        assert!(!StackNode::PushSymbol(push("send")).is_definition());
218        assert!(!file_scope().is_definition());
219    }
220
221    #[test]
222    fn only_pops_from_imports_bind_imports() {
223        let binding = ImportBinding {
224            module_stem: "utils".to_owned(),
225            source_symbol: Some("send".to_owned()),
226            relative_level: 1,
227            binds_module: false,
228        };
229        let import = StackNode::PopSymbol(SymbolPop {
230            import: Some(binding.clone()),
231            ..pop("send")
232        });
233
234        assert_eq!(import.import_binding(), Some(&binding));
235        assert!(import.is_definition());
236        assert_eq!(StackNode::PopSymbol(pop("send")).import_binding(), None);
237        assert_eq!(StackNode::PushSymbol(push("send")).import_binding(), None);
238    }
239}