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)`
41    CatchParam,
42}
43
44impl BindingKind {
45    /// The keyword or role, as it appears in a rule's `bindingKind` check.
46    #[must_use]
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::Const => "const",
50            Self::Let => "let",
51            Self::Var => "var",
52            Self::Param => "param",
53            Self::Function => "function",
54            Self::Class => "class",
55            Self::CatchParam => "catch-param",
56        }
57    }
58}
59
60/// What an identifier resolves to.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum Binding {
63    /// The name came from an import.
64    Import {
65        /// The module specifier, exactly as written.
66        module: String,
67        /// Which export.
68        name: ImportedName,
69    },
70    /// The name was declared in this file.
71    Local(BindingKind),
72}
73
74impl Binding {
75    /// The kind as a rule sees it, with imports reported as `import`.
76    #[must_use]
77    pub const fn kind_str(&self) -> &'static str {
78        match self {
79            Self::Import { .. } => "import",
80            Self::Local(kind) => kind.as_str(),
81        }
82    }
83}
84
85/// Language-specific resolution of identifiers to bindings.
86///
87/// Implementations are expected to be cheap to call repeatedly for one file — the engine
88/// resolves per match, and a query can match many times. Building a per-file index once
89/// and reusing it is the intended shape.
90pub trait BindingResolver: Send + Sync {
91    /// What the identifier at `node` refers to, or `None` if it is not an identifier or
92    /// nothing in this file declares it.
93    fn resolve(&self, tree: &Tree, source: &str, node: Node<'_>) -> Option<Binding>;
94
95    /// Whether the identifier resolves to a binding that shadows an outer one of the same
96    /// name.
97    ///
98    /// Distinct from `resolve` returning a local binding: a name declared once, locally, is
99    /// not shadowing anything. This is specifically "there is more than one, and you got
100    /// the inner one".
101    fn is_shadowed(&self, tree: &Tree, source: &str, node: Node<'_>) -> bool;
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn binding_kinds_render_as_rules_write_them() {
110        assert_eq!(BindingKind::Const.as_str(), "const");
111        assert_eq!(BindingKind::Param.as_str(), "param");
112        assert_eq!(BindingKind::CatchParam.as_str(), "catch-param");
113    }
114
115    #[test]
116    fn imports_report_as_import_regardless_of_which_export() {
117        for name in [
118            ImportedName::Default,
119            ImportedName::Namespace,
120            ImportedName::Named("a".to_owned()),
121        ] {
122            let binding = Binding::Import {
123                module: "m".to_owned(),
124                name,
125            };
126            assert_eq!(binding.kind_str(), "import");
127        }
128    }
129
130    #[test]
131    fn local_bindings_report_their_own_kind() {
132        assert_eq!(Binding::Local(BindingKind::Const).kind_str(), "const");
133        assert_eq!(Binding::Local(BindingKind::Function).kind_str(), "function");
134    }
135}