lanekeep_types/declarations.rs
1//! One parsed declaration file, and what it says about a name.
2//!
3//! Every node kind read here was checked against `tree-sitter-typescript` 0.23.2's
4//! `node-types.json`, which is where the fields are *declared*. A hand-written sample cannot
5//! stand in for that: a sample with zero `ERROR` nodes still omits whatever the author did not
6//! think to write, and AGENTS.md records four wrong claims about this grammar produced exactly
7//! that way. The kinds a statement can declare with are read through the resolver's
8//! [`BindingResolver::declares`], which owns that walk — this file's own table, kept in
9//! parallel with it, drifted once already (#229) — and the resolver is reached through the
10//! trait, the way the oracle reaches `declaration_of`, so this crate names no language crate.
11
12use std::fmt;
13use std::sync::Arc;
14
15use lanekeep_core::FilePath;
16use lanekeep_core::tracked::ContentHash;
17use lanekeep_lang::binding::{Binding, BindingResolver, ImportedName};
18use tree_sitter::{Node, Tree};
19
20/// A declaration file this run has read and parsed.
21pub struct Declaration {
22 /// Where it was read from, relative to the project root.
23 pub path: FilePath,
24 /// The resolver this file's declarations are read with: the one the provider was probed
25 /// with, so a declaration file and the file that imported it agree on what declares a
26 /// name.
27 resolver: Arc<dyn BindingResolver>,
28 /// Its text, which every byte range in `tree` indexes.
29 pub source: String,
30 /// Its parse.
31 pub tree: Tree,
32 /// What its bytes hashed to when it was read.
33 ///
34 /// Compared, within a run, against the hash of the bytes the *asking* file's own
35 /// `FileAccess` reads: two accesses over one path can see two different files when the
36 /// file is rewritten mid-run, which is routine under `--watch`, and serving this parse
37 /// against the other access's hash would write a cache entry that describes neither
38 /// version. See `BuiltinProvider::declaration`. Load-bearing across requests too: a
39 /// provider held for a session (#191) drops an entry whose hash moved.
40 pub hash: ContentHash,
41 /// Whether its parse carries a fault anywhere — an `ERROR` node, or a `MISSING` token
42 /// the parser inserted where one was expected. `Node::has_error` at the root counts
43 /// both; an unclosed brace produces the second and no `ERROR` at all.
44 ///
45 /// `tree_sitter::Parser::parse` answers a tree for any UTF-8 input, so a file this
46 /// provider could not really read is indistinguishable from one it read cleanly unless
47 /// the question is asked here. The arms still answer whatever the tree does hold — a
48 /// declaration outside the damaged span is a real declaration — and it is `complete()`
49 /// that turns this into the honest label on a partial answer.
50 ///
51 /// **The per-declaration granularity is the reached node's own `has_error()`**, asked
52 /// by the walk of the declaration it ends at; this whole-file flag is what a nameless
53 /// import keeps, and what a name the walk cannot follow to a declaration falls back to.
54 pub has_error: bool,
55}
56
57impl fmt::Debug for Declaration {
58 /// `source` is summarized rather than printed, the same call the oracle's own `Debug`
59 /// makes: a whole declaration file in every log line the value appears in is not one
60 /// anybody can read, and its length identifies which file this is as well as the bytes do.
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.debug_struct("Declaration")
63 .field("path", &self.path)
64 .field("source_len", &self.source.len())
65 .field("hash", &self.hash)
66 .field("has_error", &self.has_error)
67 .finish_non_exhaustive()
68 }
69}
70
71impl Declaration {
72 /// Parse a declaration file that has already been read through a tracked access.
73 ///
74 /// Takes the parser rather than making one: a provider owns exactly one for the run, and
75 /// a constructor that made its own would put a second parser behind an API that reads
76 /// like it could not.
77 #[must_use]
78 pub fn parse(
79 path: FilePath,
80 source: String,
81 parser: &mut tree_sitter::Parser,
82 resolver: Arc<dyn BindingResolver>,
83 ) -> Option<Self> {
84 let hash = ContentHash::new(*blake3::hash(source.as_bytes()).as_bytes());
85 let tree = parser.parse(&source, None)?;
86 let has_error = tree.root_node().has_error();
87 Some(Self {
88 path,
89 resolver,
90 source,
91 tree,
92 hash,
93 has_error,
94 })
95 }
96}
97
98/// Where a file that declares an export sends the name it was asked about.
99///
100/// Four variants rather than a bare node, because three of the shapes the grammar produces
101/// are not nodes in this file at all: a named re-export and a star re-export both name another
102/// module, and a namespace re-export names a whole module object that has no declaration to
103/// point at.
104#[derive(Debug)]
105pub enum Exported<'d> {
106 /// Declared in this file, at this node.
107 Here(Node<'d>),
108 /// Re-exported by name: `export { A as B } from './x'`, asked about `B`, yields `A`.
109 From {
110 /// The module specifier exactly as written.
111 specifier: String,
112 /// The name to ask that module for.
113 name: String,
114 },
115 /// `export * as ns from './x'` — bound to the whole module object, which has no single
116 /// declaration and no type this oracle can build.
117 Namespace {
118 /// The module specifier exactly as written.
119 specifier: String,
120 },
121 /// Not named here. Each of these `export * from` sources may have it, in source order.
122 Star(Vec<String>),
123}
124
125/// Where the file's own file-level export named `name` leads.
126///
127/// Explicit exports first, in source order, and the `export *` fallback only when nothing
128/// explicit answered — which is what TypeScript itself does, and what keeps a name a file
129/// declares from being answered by a different declaration elsewhere that happens to share
130/// its spelling.
131#[must_use]
132pub fn find_export<'d>(decl: &'d Declaration, name: &str) -> Option<Exported<'d>> {
133 let root = decl.tree.root_node();
134 let mut cursor = root.walk();
135 let mut stars = Vec::new();
136
137 for statement in root.named_children(&mut cursor) {
138 if statement.kind() != "export_statement" {
139 continue;
140 }
141 let source = statement
142 .child_by_field_name("source")
143 .map(|node| unquote(text(decl, node)).to_owned());
144
145 if let Some(specifier) = source {
146 if let Some(clause) = named_child_of_kind(statement, "export_clause") {
147 if let Some(exported) = clause_target(decl, clause, name, &specifier) {
148 return Some(exported);
149 }
150 continue;
151 }
152 if let Some(namespace) = named_child_of_kind(statement, "namespace_export") {
153 if namespace
154 .named_child(0)
155 .is_some_and(|n| unquote(text(decl, n)) == name)
156 {
157 return Some(Exported::Namespace { specifier });
158 }
159 continue;
160 }
161 // `export * from 'm'`: a source, no clause, no namespace. Collected rather than
162 // followed, so an explicit export later in the file still wins.
163 stars.push(specifier);
164 continue;
165 }
166
167 // A local re-export, `export { A }` or `export { A as B }`. The local name is either
168 // declared in this file or bound by one of its imports — `import { A } from './a';
169 // export { A };` is the two-statement barrel, and the chain continues into `./a`
170 // under the module's own spelling of the name. The resolver says which, the way it
171 // answers a use anywhere else: a walk outward from the specifier's own identifier.
172 if let Some(clause) = named_child_of_kind(statement, "export_clause")
173 && let Some(local) = local_clause_node(decl, clause, name)
174 {
175 if let Some(node) = declared_here(decl, unquote(text(decl, local))) {
176 return Some(Exported::Here(node));
177 }
178 if let Some(Binding::Import {
179 module,
180 name: imported,
181 }) = decl.resolver.resolve(&decl.tree, &decl.source, local)
182 {
183 return Some(match imported {
184 ImportedName::Named(exported) => Exported::From {
185 specifier: module,
186 name: exported,
187 },
188 ImportedName::Default => Exported::From {
189 specifier: module,
190 name: "default".to_owned(),
191 },
192 ImportedName::Namespace => Exported::Namespace { specifier: module },
193 });
194 }
195 }
196
197 let is_default = anonymous_child(statement, "default");
198 // `export = X`, whose only marker is the `=` token: no field, an `expression` child.
199 // Treated as the default export, which is what an `import D from 'm'` binds under
200 // `esModuleInterop` — the spelling every consumer of such a module writes.
201 let is_export_assignment = anonymous_child(statement, "=");
202
203 if let Some(declaration) = statement.child_by_field_name("declaration") {
204 let wanted = if is_default { "default" } else { name };
205 if is_default && name == "default" {
206 return Some(Exported::Here(unwrap_ambient(declaration)));
207 }
208 if !is_default && let Some(node) = declares(decl, declaration, wanted) {
209 return Some(Exported::Here(node));
210 }
211 continue;
212 }
213
214 if (is_default || is_export_assignment) && name == "default" {
215 let value = statement
216 .child_by_field_name("value")
217 .or_else(|| statement.named_children(&mut statement.walk()).next())?;
218 // `export default Big` names a declaration; `export default 1` is the value
219 // itself, which the oracle types directly.
220 if value.kind() == "identifier"
221 && let Some(node) = declared_here(decl, text(decl, value))
222 {
223 return Some(Exported::Here(node));
224 }
225 return Some(Exported::Here(value));
226 }
227 }
228
229 (!stars.is_empty()).then_some(Exported::Star(stars))
230}
231
232/// The declaration of `name` at this file's top level, exported or not.
233///
234/// Both spellings, because a chain ends at whichever one the declaring file used:
235/// `export declare class Big {}` and `declare class Big {}` + `export default Big` name the
236/// same thing, and a walk that reached only through `export_statement` would find the first
237/// and lose the second.
238#[must_use]
239pub fn declared_here<'d>(decl: &'d Declaration, name: &str) -> Option<Node<'d>> {
240 declared_in(decl.resolver.as_ref(), &decl.tree, &decl.source, name)
241}
242
243/// The declaration of `name` at a parsed file's top level, exported or not.
244///
245/// [`declared_here`] is this over a [`Declaration`]; this is the same walk over a tree the
246/// provider does not own — the file under check, which the engine already parsed and which
247/// must not be parsed a second time — so the resolver comes in from the caller, which holds
248/// the provider's.
249#[must_use]
250pub(crate) fn declared_in<'t>(
251 resolver: &dyn BindingResolver,
252 tree: &'t Tree,
253 source: &'t str,
254 name: &str,
255) -> Option<Node<'t>> {
256 let root = tree.root_node();
257 let mut cursor = root.walk();
258 for statement in root.named_children(&mut cursor) {
259 let candidate = if statement.kind() == "export_statement" {
260 match statement.child_by_field_name("declaration") {
261 Some(declaration) => declaration,
262 // A re-export carries no declaration of its own; the name may still be
263 // declared further down, so skip rather than give up on the file.
264 None => continue,
265 }
266 } else {
267 statement
268 };
269 if let Some(found) = resolver.declares(source, candidate, name) {
270 return Some(found);
271 }
272 }
273 None
274}
275
276/// The name a declaration node declares, when it declares one.
277///
278/// `None` for an anonymous default export — `export default 1`, `export default () => {}` —
279/// which is a real shape rather than a gap: it has no name, so a `symbolOf` following a chain
280/// to it has nothing better to report than `default`.
281///
282/// `None` for a destructured declarator too: `export const { e } = { e: 2 }` binds a
283/// *pattern*, whose text is `{ e }` — and reporting a shape where a spelling belongs would
284/// put `exported: "{ e }"` on a `Symbol`, which a rule comparing against a required export
285/// name would read as a mismatch on conforming code. `walk_export` falls back to the name
286/// the chain was asked for, which is what a shorthand destructuring exports.
287///
288/// A dotted namespace's name is its first segment: `namespace A.B {}` declares `A`, the
289/// same name the resolver binds for it, and `A.B` is a spelling no `import` can write.
290#[must_use]
291pub fn declared_name(decl: &Declaration, node: Node<'_>) -> Option<String> {
292 let node = unwrap_ambient(node);
293 node.child_by_field_name("name")
294 .filter(|name| {
295 matches!(
296 name.kind(),
297 "identifier" | "type_identifier" | "nested_identifier" | "string"
298 )
299 })
300 .map(|name| unquote(text(decl, first_segment(name))).to_owned())
301}
302
303/// The node whose text is the declared name: the leftmost segment of a `nested_identifier`.
304///
305/// `namespace A.B {}` is shorthand for `namespace A { namespace B {} }` — the enclosing
306/// scope sees `A`, and `lanekeep-lang-js` binds it so. The grammar aliases every inner level
307/// of a dotted name to `member_expression`, so `google.maps.places` is a `nested_identifier`
308/// over the member expression `google.maps`; both kinds carry an `object` field. Anything
309/// that is not nested is its own name.
310fn first_segment(name: Node<'_>) -> Node<'_> {
311 let mut node = name;
312 while matches!(node.kind(), "nested_identifier" | "member_expression") {
313 match node.child_by_field_name("object") {
314 Some(object) => node = object,
315 None => break,
316 }
317 }
318 node
319}
320
321/// Whether `declaration` declares `name`, and where.
322///
323/// The resolver's walk — one table for both crates, where a second once drifted (#229).
324/// Imports are filtered out on the resolver's side, so an `import_statement` never answers
325/// as a declaration here.
326fn declares<'d>(decl: &'d Declaration, declaration: Node<'d>, name: &str) -> Option<Node<'d>> {
327 decl.resolver.declares(&decl.source, declaration, name)
328}
329
330/// Step through `ambient_declaration`, which wraps the declaration `declare` applies to.
331///
332/// The first named child that is not a `comment`: a comment is a named extra in this
333/// grammar, so `declare /** doc */ class C {}` puts one ahead of the class, and a `.d.ts`
334/// writes that shape all the time.
335fn unwrap_ambient(node: Node<'_>) -> Node<'_> {
336 if node.kind() != "ambient_declaration" {
337 return node;
338 }
339 let mut cursor = node.walk();
340 node.named_children(&mut cursor)
341 .find(|child| child.kind() != "comment")
342 .unwrap_or(node)
343}
344
345/// The re-export target for `name` inside `export { … } from 'm'`.
346fn clause_target<'d>(
347 decl: &'d Declaration,
348 clause: Node<'d>,
349 name: &str,
350 specifier: &str,
351) -> Option<Exported<'d>> {
352 let mut cursor = clause.walk();
353 for specifier_node in clause.named_children(&mut cursor) {
354 if specifier_node.kind() != "export_specifier" {
355 continue;
356 }
357 let exported = specifier_node.child_by_field_name("name")?;
358 let visible = specifier_node
359 .child_by_field_name("alias")
360 .unwrap_or(exported);
361 if unquote(text(decl, visible)) == name {
362 return Some(Exported::From {
363 specifier: specifier.to_owned(),
364 name: unquote(text(decl, exported)).to_owned(),
365 });
366 }
367 }
368 None
369}
370
371/// The `name` node of the `export_specifier` in a local `export { … }` that exports `name`.
372///
373/// The node rather than its text, because the resolver resolves a *node* by walking outward
374/// from it — which is what lets [`find_export`] ask whether the local name is an import
375/// binding rather than a declaration.
376fn local_clause_node<'d>(decl: &Declaration, clause: Node<'d>, name: &str) -> Option<Node<'d>> {
377 let mut cursor = clause.walk();
378 for specifier in clause.named_children(&mut cursor) {
379 if specifier.kind() != "export_specifier" {
380 continue;
381 }
382 let local = specifier.child_by_field_name("name")?;
383 let visible = specifier.child_by_field_name("alias").unwrap_or(local);
384 if unquote(text(decl, visible)) == name {
385 return Some(local);
386 }
387 }
388 None
389}
390
391/// The file and name a chain of re-exports ends at.
392///
393/// `name` is the name the *declaring* file uses, which is what `symbolOf` reports as
394/// `exported` — so `export { Decimal as Big }` asked about `Big` answers `Decimal`, and a
395/// rule comparing against a required export name compares against the real one rather than
396/// against whatever spelling the last hop chose.
397#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
398pub struct ExportTarget {
399 /// The declaring file, relative to the project root.
400 pub file: FilePath,
401 /// The name it declares the export under.
402 pub name: String,
403}
404
405/// The node an already-resolved [`ExportTarget`] names, in its own file.
406///
407/// `declared_here` first, because a chain ends at whichever spelling the declaring file
408/// used and `declare class Big {}` is not an `export_statement` at all. `find_export` is
409/// the fallback for the one shape that has no name to look up: an anonymous default.
410///
411/// A node the parser only partly read — `has_error()`, which counts a `MISSING` token as
412/// well as an `ERROR` — answers `None` rather than the node: a damaged declaration has no
413/// shape worth typing. `walk_export` makes the same refusal for the chain it walks; this is
414/// the refusal for the callers that look a name up here directly, so the two cannot
415/// disagree about one node.
416#[must_use]
417pub(crate) fn target_node<'d>(decl: &'d Declaration, name: &str) -> Option<Node<'d>> {
418 declared_here(decl, name)
419 .or_else(|| match find_export(decl, name) {
420 Some(Exported::Here(node)) => Some(node),
421 _ => None,
422 })
423 .filter(|node| !node.has_error())
424}
425
426/// The first named child of `kind`, when there is one.
427fn named_child_of_kind<'d>(node: Node<'d>, kind: &str) -> Option<Node<'d>> {
428 let mut cursor = node.walk();
429 node.named_children(&mut cursor).find(|c| c.kind() == kind)
430}
431
432/// Whether an anonymous token of this text is a direct child.
433///
434/// `default` and `=` are the only markers separating three otherwise identical shapes of
435/// `export_statement`, and neither is a field — the grammar writes them as bare tokens
436/// (`common/define-grammar.js:329`, `tree-sitter-javascript`'s `grammar.js:185`).
437fn anonymous_child(node: Node<'_>, token: &str) -> bool {
438 let mut cursor = node.walk();
439 node.children(&mut cursor)
440 .any(|child| !child.is_named() && child.kind() == token)
441}
442
443/// The source text of a node.
444fn text<'d>(decl: &'d Declaration, node: Node<'_>) -> &'d str {
445 text_of(&decl.source, node)
446}
447
448/// The source text of a node, read from a source string directly rather than a
449/// [`Declaration`] — what [`imports_with_names`] needs over a tree it does not own.
450fn text_of<'t>(source: &'t str, node: Node<'_>) -> &'t str {
451 source.get(node.byte_range()).unwrap_or("")
452}
453
454/// Drop the quotes a string module name or a string export name carries.
455///
456/// `export_specifier`'s `name` and `alias` may be a `string` as well as an `identifier`
457/// (`node-types.json`), and so may a module's `source`, so every read of one goes through
458/// this rather than through four places that each remember to.
459fn unquote(text: &str) -> &str {
460 let bytes = text.as_bytes();
461 match (bytes.first(), bytes.last()) {
462 (Some(b'"' | b'\''), Some(b'"' | b'\'')) if text.len() >= 2 => &text[1..text.len() - 1],
463 _ => text,
464 }
465}
466
467/// One import statement's specifier, with the names it binds.
468///
469/// `complete`'s per-name walk needs more than the bare specifier the old whole-file walk
470/// returned: an `ERROR` verdict is now decided per *reached* declaration, and which
471/// declarations an import reaches is exactly its name list. A nameless import —
472/// side-effect, `import * as ns`, `export *` — binds no single declaration to reach (a
473/// namespace binds the whole module object, which the caller judges the same way) and
474/// keeps the whole-file verdict; see the caller.
475#[derive(Debug)]
476pub(crate) struct ImportedSpecifier {
477 /// The module specifier exactly as written, quotes stripped.
478 pub specifier: String,
479 /// The names the statement binds, in source order — empty for a nameless one.
480 pub names: Vec<ImportedName>,
481}
482
483/// Every module specifier this file imports from, in source order, with the names each
484/// statement binds.
485///
486/// `import_statement`'s `source` field, plus `export_statement`'s: a barrel file re-exporting
487/// what it never imports is exactly as dependent on those modules, and a completeness answer
488/// that ignored them would call such a file complete while knowing nothing about it.
489///
490/// The name lists are read off the grammar the same way `JsBindingResolver`'s
491/// `import_binding` reads them — the two were dumped side by side against
492/// `tree-sitter-typescript` 0.23.2 rather than trusted from a sample, which is where the
493/// default/namespace/named split below comes from.
494///
495/// `import x = require('m')` needs its own fallback: `node-types.json` marks
496/// `import_statement`'s own `source` field `required: false` and puts the field this shape
497/// actually carries on its `import_require_clause` child instead — so a bare
498/// `child_by_field_name("source")` on the statement itself answers nothing for exactly this
499/// one shape, silently dropping it from the count. It binds one name by assignment rather
500/// than by an `import_clause`, so it contributes an empty name list.
501#[must_use]
502pub(crate) fn imports_with_names(tree: &Tree, source: &str) -> Vec<ImportedSpecifier> {
503 let root = tree.root_node();
504 let mut cursor = root.walk();
505 root.named_children(&mut cursor)
506 .filter(|statement| matches!(statement.kind(), "import_statement" | "export_statement"))
507 .filter_map(|statement| {
508 let specifier = statement.child_by_field_name("source").or_else(|| {
509 named_child_of_kind(statement, "import_require_clause")
510 .and_then(|clause| clause.child_by_field_name("source"))
511 })?;
512 Some(ImportedSpecifier {
513 specifier: unquote(text_of(source, specifier)).to_owned(),
514 names: bound_names(statement, source),
515 })
516 })
517 .collect()
518}
519
520/// The names one `import_statement` or `export_statement` binds from its `source` module.
521///
522/// A statement without a source module — a local `export { A }` — binds nothing from
523/// anywhere, and never reaches this function.
524fn bound_names(statement: Node<'_>, source: &str) -> Vec<ImportedName> {
525 match statement.kind() {
526 "import_statement" => {
527 let Some(clause) = named_child_of_kind(statement, "import_clause") else {
528 return Vec::new();
529 };
530 let mut names = Vec::new();
531 let mut cursor = clause.walk();
532 for child in clause.children(&mut cursor) {
533 match child.kind() {
534 // `import d from 'm'` — and the `d` of `import d, * as ns from 'm'`.
535 "identifier" => names.push(ImportedName::Default),
536 // `import * as ns from 'm'`
537 "namespace_import" => names.push(ImportedName::Namespace),
538 // `import { a, b as c } from 'm'`
539 "named_imports" => {
540 let mut inner = child.walk();
541 for specifier in child
542 .children(&mut inner)
543 .filter(|s| s.kind() == "import_specifier")
544 {
545 // The module's own spelling, not the local alias: the walk that
546 // follows asks the declaring module what it exports.
547 if let Some(exported) = specifier.child_by_field_name("name") {
548 names.push(ImportedName::Named(
549 unquote(text_of(source, exported)).to_owned(),
550 ));
551 }
552 }
553 }
554 _ => {}
555 }
556 }
557 names
558 }
559 "export_statement" => {
560 // `export { A as B } from 'm'` — `A` is the name in the *other* module, which is
561 // the one a walk from here asks it for.
562 if let Some(clause) = named_child_of_kind(statement, "export_clause") {
563 let mut names = Vec::new();
564 let mut cursor = clause.walk();
565 for specifier in clause
566 .named_children(&mut cursor)
567 .filter(|s| s.kind() == "export_specifier")
568 {
569 if let Some(exported) = specifier.child_by_field_name("name") {
570 names.push(ImportedName::Named(
571 unquote(text_of(source, exported)).to_owned(),
572 ));
573 }
574 }
575 return names;
576 }
577 // `export * as ns from 'm'`
578 if named_child_of_kind(statement, "namespace_export").is_some() {
579 return vec![ImportedName::Namespace];
580 }
581 // Bare `export * from 'm'`: nothing is bound, so nothing is reached.
582 Vec::new()
583 }
584 _ => Vec::new(),
585 }
586}