Skip to main content

lanekeep_lang/
binding.rs

1//! What an identifier refers to.
2//!
3//! This is the "light binding resolution" of architecture ยง1 โ€” deliberately not type-aware
4//! analysis. It answers one question: given an identifier, where does the name come from?
5//!
6//! That question is what pure syntactic matching gets wrong. A rule looking for
7//! `makeStyles(...)` by matching the identifier text is wrong twice over: it misses
8//! `import { makeStyles as ms }` and it fires on a local `const makeStyles = ...` that has
9//! nothing to do with the import. Both are ordinary things to write, and both produce
10//! results a user reads as the tool being broken.
11
12use tree_sitter::{Node, Tree};
13
14/// Which export of a module a name came from.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ImportedName {
17    /// `import d from 'm'`
18    Default,
19    /// `import * as ns from 'm'`
20    Namespace,
21    /// `import { a } from 'm'`, or `import { a as b }` where this is `a`.
22    Named(String),
23}
24
25/// How a local name was introduced.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BindingKind {
28    /// `const x = ...`
29    Const,
30    /// `let x = ...`
31    Let,
32    /// `var x = ...`
33    Var,
34    /// A function or method parameter.
35    Param,
36    /// `function x() {}`
37    Function,
38    /// `class X {}`
39    Class,
40    /// `catch (e)`, and Python's `except E as e`.
41    CatchParam,
42
43    // --- forms with no JavaScript equivalent -------------------------------------------
44    //
45    // Python binds by assigning, and has no keyword to distinguish `const` from `let`. The
46    // kinds below say *how* a name came to be bound, which is the question a rule can act
47    // on โ€” reusing `var` for all of them would answer it with something untrue.
48    /// `x = 1`, an augmented assignment, or a walrus `x := 1`.
49    Assignment,
50    /// The target of a `for` statement or a `for` clause.
51    Loop,
52    /// `with open(p) as f`.
53    ContextManager,
54    /// A comprehension's own target, which is scoped to the comprehension.
55    Comprehension,
56}
57
58impl BindingKind {
59    /// The keyword or role, as it appears in a rule's `bindingKind` check.
60    #[must_use]
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::Const => "const",
64            Self::Let => "let",
65            Self::Var => "var",
66            Self::Param => "param",
67            Self::Function => "function",
68            Self::Class => "class",
69            Self::CatchParam => "catch-param",
70            Self::Assignment => "assignment",
71            Self::Loop => "loop",
72            Self::ContextManager => "context-manager",
73            Self::Comprehension => "comprehension",
74        }
75    }
76}
77
78/// What an identifier resolves to.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum Binding {
81    /// The name came from an import.
82    Import {
83        /// The module specifier, exactly as written.
84        module: String,
85        /// Which export.
86        name: ImportedName,
87    },
88    /// The name was declared in this file.
89    Local(BindingKind),
90}
91
92impl Binding {
93    /// The kind as a rule sees it, with imports reported as `import`.
94    #[must_use]
95    pub const fn kind_str(&self) -> &'static str {
96        match self {
97            Self::Import { .. } => "import",
98            Self::Local(kind) => kind.as_str(),
99        }
100    }
101}
102
103/// Language-specific resolution of identifiers to bindings.
104///
105/// Implementations are expected to be cheap to call repeatedly for one file โ€” the engine
106/// resolves per match, and a query can match many times. Building a per-file index once
107/// and reusing it is the intended shape.
108pub trait BindingResolver: Send + Sync {
109    /// What the identifier at `node` refers to, or `None` if it is not an identifier or
110    /// nothing in this file declares it.
111    fn resolve(&self, tree: &Tree, source: &str, node: Node<'_>) -> Option<Binding>;
112
113    /// Whether the identifier resolves to a binding that shadows an outer one of the same
114    /// name.
115    ///
116    /// Distinct from `resolve` returning a local binding: a name declared once, locally, is
117    /// not shadowing anything. This is specifically "there is more than one, and you got
118    /// the inner one".
119    fn is_shadowed(&self, tree: &Tree, source: &str, node: Node<'_>) -> bool;
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn binding_kinds_render_as_rules_write_them() {
128        assert_eq!(BindingKind::Const.as_str(), "const");
129        assert_eq!(BindingKind::Param.as_str(), "param");
130        assert_eq!(BindingKind::CatchParam.as_str(), "catch-param");
131    }
132
133    #[test]
134    fn imports_report_as_import_regardless_of_which_export() {
135        for name in [
136            ImportedName::Default,
137            ImportedName::Namespace,
138            ImportedName::Named("a".to_owned()),
139        ] {
140            let binding = Binding::Import {
141                module: "m".to_owned(),
142                name,
143            };
144            assert_eq!(binding.kind_str(), "import");
145        }
146    }
147
148    #[test]
149    fn local_bindings_report_their_own_kind() {
150        assert_eq!(Binding::Local(BindingKind::Const).kind_str(), "const");
151        assert_eq!(Binding::Local(BindingKind::Function).kind_str(), "function");
152    }
153}