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}