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}