Skip to main content

lanekeep_types/
provider.rs

1//! The seam every type answer crosses.
2//!
3//! [`TypeScriptOracle`](crate::TypeScriptOracle) borrows one tree for its whole life, which
4//! is the right shape for a within-file oracle and the wrong one for anything
5//! whole-program: a provider backed by a compiler holds state the *run* owns and cannot hand
6//! back a value borrowing a single parse. So the boundary is a trait whose methods borrow
7//! nothing beyond the call, and everything a question needs travels in one [`Query`].
8//!
9//! Every arm may answer `None`, and `None` is this crate's first-class "I could not be sure"
10//! rather than a failure — see the crate documentation. A provider that guessed would
11//! produce a rule that accuses correct code, which is the one failure the whole surface is
12//! arranged against.
13
14use std::collections::BTreeSet;
15
16use lanekeep_core::files::FileAccess;
17use lanekeep_core::{AnalysisBudget, FilePath};
18
19use crate::types::{Symbol, Type};
20
21/// One question, with everything answering it needs.
22///
23/// `Copy`, so an arm can hand the same question to a helper without a clone and without
24/// borrowing itself into a corner.
25#[derive(Debug, Clone, Copy)]
26pub struct Query<'a> {
27    /// The file the question is about, relative to the project root.
28    ///
29    /// A relative specifier resolves against this, so it is load-bearing rather than
30    /// informational: the same import written in two directories names two files.
31    pub file: &'a FilePath,
32    /// That file's parse.
33    pub tree: &'a tree_sitter::Tree,
34    /// That file's source, which every byte range in `tree` indexes.
35    pub source: &'a str,
36    /// The node asked about. The file's root for [`TypeProvider::complete`], which asks
37    /// about the whole file rather than about a position in it.
38    pub node: tree_sitter::Node<'a>,
39    /// Tracked, confined access to the rest of the project.
40    ///
41    /// Every file a provider opens goes through this, hit or miss, so a declaration an
42    /// answer depended on — including one that was **not** there — invalidates the asking
43    /// file's cache entry when it appears, changes or vanishes. A provider reading the
44    /// filesystem any other way would compute an answer no cache key covers.
45    pub files: &'a FileAccess,
46}
47
48/// What answers `ctx.types`.
49///
50/// One implementation per strategy — the bounded builtin oracle here, a `tsc` sidecar in
51/// A3 — and the engine holds exactly one for a run. [`Self::identity`] is why the two cannot
52/// be confused by a cache: it is a key input, so a result computed by one is never served to
53/// a run using the other.
54pub trait TypeProvider: Send + Sync {
55    /// The type of the expression at `q.node`.
56    fn type_of(&self, q: Query<'_>) -> Option<Type>;
57
58    /// Where the name at `q.node` came from.
59    fn symbol_of(&self, q: Query<'_>) -> Option<Symbol>;
60
61    /// What calling the function at `q.node` yields.
62    ///
63    /// Separate from [`Self::type_of`] because a function declaration is not an expression,
64    /// and giving `type_of` a signature type would invent a variant every rule would then
65    /// have to unpack. Accepts a call expression, a function-like declaration, or an
66    /// identifier bound to one.
67    fn return_type_of(&self, q: Query<'_>) -> Option<Type>;
68
69    /// Whether the type at `q.node` is the type `module` exports as `name`, or declares a
70    /// relationship to it — `extends` or `implements` — across files, through aliases of the
71    /// named type.
72    ///
73    /// Nominal, never structural. `Some(false)` is a real answer — the walk completed and
74    /// found nothing — and `None` is "a link in the chain could not be read", which a rule
75    /// must not treat as a negative. A union is assignable only when every member is.
76    ///
77    /// Three narrowings of what "found nothing" honestly covers: declaration merging is not
78    /// followed, so when a name is declared more than once at a file's top level only the
79    /// first declaration is consulted; a generic annotation at the use site (`let x:
80    /// Box<number>`) answers `None`, since type arguments are not read; and a `name` the
81    /// named `module` does not export answers `None` rather than `Some(false)`, because
82    /// `Some(false)` there would make a requirement rule report on every value the module
83    /// never claimed to type.
84    ///
85    /// One documented gap: declarations are looked up at a file's top level only, so a
86    /// declaration that shadows the target's name inside a function body is not
87    /// distinguished from the top-level one with that name — the walk answers as if the
88    /// shadow were the top-level declaration.
89    fn is_assignable_to(&self, q: Query<'_>, module: &str, name: &str) -> Option<bool>;
90
91    /// Whether every import in `q`'s file resolved to something this provider could read.
92    ///
93    /// `false` is the honest label on a partial answer: a rule that reports on an absent
94    /// type would accuse code the provider never saw. Takes a whole [`Query`] rather than a
95    /// path because the question is about the file's *imports*, which cannot be enumerated
96    /// without its tree.
97    ///
98    /// **The verdict is about reading, per declaration where one is reached.** A parse fault
99    /// anywhere in a resolved declaration file used to mark every importer of it incomplete —
100    /// one unparsed construct in a fifty-thousand-line `@types` bundle, poisoning the whole
101    /// project. The builtin provider judges a named import by the node its name reaches: a
102    /// fault inside that node — an `ERROR`, or the `MISSING` token an unclosed brace leaves —
103    /// makes the file incomplete, one elsewhere does not, and the declarations outside the
104    /// damaged span answer normally. A name whose module resolved and parsed cleanly but which
105    /// the provider cannot follow to a declaration is *not* incomplete: `complete` says whether
106    /// every import was read, never whether every name is typeable, and `export = X` beside
107    /// `declare namespace X` is a module read whole whose members answer `undefined`. A
108    /// nameless import — side-effect, namespace, `export *` — has no single node to reach and
109    /// keeps the whole-file verdict. Silence is still the safe direction: a link that cannot
110    /// be read, wherever it sits, is `false` rather than a guess. A provider that delegates to
111    /// `tsc` answers whatever its driver reports for the file as a whole.
112    fn complete(&self, q: Query<'_>) -> bool;
113
114    /// What this provider *is*, for the cache key.
115    ///
116    /// Folded into `analysis_hash` by the engine. Two providers that would answer
117    /// differently must return different bytes, and a provider whose own code changed must
118    /// too — the builtin one derives it from `oracle_identity`, which digests this crate's
119    /// `src/`, for exactly that reason.
120    fn identity(&self) -> Vec<u8>;
121
122    /// Every path this provider's own dependency mechanism named for the run it last prepared.
123    ///
124    /// `--watch`'s allowlist unions this with `Outcome::dependency_paths`, because the two
125    /// cover different providers' dependencies and neither can stand in for the other. A
126    /// provider whose reads travel through [`Query::files`] — the builtin oracle — already
127    /// lands in `Outcome::dependency_paths` as per-file tracked reads, so its default here is
128    /// empty rather than a duplicate of what the engine already collects. The `tsc` provider
129    /// overrides it: the compiler reads through its own host, never through `Query::files`, so
130    /// nothing it consults is a tracked read on any file, and without this override an edit to
131    /// a linked package's declaration file would wake `--watch` for nothing at all.
132    ///
133    /// Answered after [`Self::begin_run`], from whatever it last built — a provider held across
134    /// runs (plan 6) reports the run it most recently prepared, not the run before it.
135    ///
136    /// The default is empty, matching every provider that has no dependency mechanism outside
137    /// tracked reads.
138    fn dependency_paths(&self) -> BTreeSet<FilePath> {
139        BTreeSet::new()
140    }
141
142    /// Drop whatever this provider holds that the files no longer support.
143    ///
144    /// Called once per request by a session that holds this provider across several of them
145    /// (`crates/lanekeep-cli/src/session.rs`). A one-shot run builds a provider and throws it
146    /// away, so for that path this is a no-op that costs a virtual call.
147    ///
148    /// The default body is empty because a provider with no cache has nothing to revalidate,
149    /// and requiring every implementor to write that down would be noise. The two that do
150    /// hold something override it: the builtin provider re-hashes each declaration it parsed
151    /// and forgets its memoized completeness, and the `tsc` provider does nothing here because
152    /// `begin_run` already re-answers `programs` on every prepare, held provider or not.
153    /// **Nothing here reads an mtime** — held state is keyed by content hash, which is the
154    /// only reason it is allowed to be held at all (architecture §8.2).
155    fn revalidate(&self, files: &FileAccess) {
156        let _ = files;
157    }
158
159    /// Prepare for a run over the corpus `files` yields, and answer the per-run key term.
160    ///
161    /// Called by the engine once per run, after discovery and before the run key is
162    /// computed, for a fresh provider and a held one alike (plan 6). The default is nothing
163    /// to build and no term: this provider's dependencies are tracked reads on each entry.
164    /// The `tsc` provider (plan 5) overrides it to build every program the run's files
165    /// belong to and answer the hash over their file lists and contents — that provider's
166    /// whole dependency mechanism (spec §5.6) — which is why the term is asked for through
167    /// the trait rather than read off a concrete type the engine would have to downcast to.
168    /// The run's [`AnalysisBudget`] is the second parameter: a provider that spends wall
169    /// clock time preparing must spend the *run's* budget rather than one of its own, so a
170    /// breach here is the same breach `timeouts.analysis` names everywhere else. The default
171    /// body ignores it for the same reason it ignores the file list — it does no work.
172    ///
173    /// **The list is a closure, and the default body never calls it.** Producing it costs the
174    /// engine a second walk of the whole project, on the warm path, for a provider that may
175    /// have no use for it — which is what the only provider that ships today does. A `&[…]`
176    /// parameter makes that walk unconditional; a `&dyn Fn` makes it the caller's cost only
177    /// when a provider asks.
178    ///
179    /// What the closure yields is the corpus **as discovered**, not the run's `--since` or
180    /// `--staged` selection: a program is a property of the project, and building one from a
181    /// changed-files subset would answer a different question on a warm run than on a cold
182    /// one.
183    ///
184    /// # Errors
185    ///
186    /// [`BeginRunError::Timeout`] when the work outlived its budget and
187    /// [`BeginRunError::Failed`] for anything else. Both cancel the run, and the engine takes
188    /// a different exit for each: `RunError::AnalysisTimeout`, which names `timeouts.analysis`,
189    /// and `RunError::Provider`, which names the toolchain.
190    fn begin_run(
191        &self,
192        files: &dyn Fn() -> Vec<FilePath>,
193        budget: AnalysisBudget,
194    ) -> Result<Vec<u8>, BeginRunError> {
195        let _ = (files, budget);
196        Ok(Vec::new())
197    }
198
199    /// The first error this provider produced, if it has produced one.
200    ///
201    /// **Why this is on the trait at all.** Every arm above answers "I don't know" — `None`,
202    /// or `false` from [`Self::complete`] — when the provider is broken, because the trait has
203    /// nowhere to put an error. So a killed sidecar is indistinguishable, at this boundary,
204    /// from a compiler that simply has no type for that node: the file finishes *degraded* and
205    /// is committed under a valid cache key, which is a limit degrading a run instead of
206    /// cancelling it. This is the one way a caller can tell the two apart, so the engine asks
207    /// after every file and cancels the run when it is `Some`.
208    ///
209    /// It is answered through the trait rather than off a concrete type because the engine
210    /// holds an `Arc<dyn TypeProvider>` — a session's held provider (plan 6) included — and
211    /// the alternative is a downcast, which is the door plan 4 closed for the run key.
212    ///
213    /// [`BeginRunError`] rather than an enum of its own: its two arms are exactly the two
214    /// exits the engine has for a provider — a spent `timeouts.analysis`, and everything else
215    /// — and a second type with the same two arms would be two spellings of one decision.
216    ///
217    /// The default is `None`: a provider with no out-of-band state has nothing to report, and
218    /// the builtin one has no analogous failure.
219    fn failure(&self) -> Option<BeginRunError> {
220        None
221    }
222
223    /// Lines this provider wants said about the run it has just prepared.
224    ///
225    /// Asked once, after [`Self::begin_run`], and printed by the CLI on **stderr** — never on
226    /// stdout, which carries a run's report and is what a machine reads. The engine prints
227    /// nothing itself; it exposes these and the caller decides.
228    ///
229    /// What they are for is a decision a provider made silently that changes its answers. The
230    /// `tsc` provider's is the ad-hoc program: a file no `tsconfig.json` *under the project
231    /// root* claims is typed with this driver's own options rather than the project's, so
232    /// `strict` is off for it and the same file checked from one directory up answers
233    /// differently. That is not a failure — nothing is wrong and the run is correct — so it
234    /// cannot be an error, and it is not nothing either.
235    ///
236    /// The default is empty: a provider that made no such decision has nothing to say, and a
237    /// notice printed by every run is noise rather than information.
238    fn notices(&self) -> Vec<String> {
239        Vec::new()
240    }
241
242    /// Whether this provider is past repair and has to be built again.
243    ///
244    /// Asked by a session that holds a provider across requests
245    /// (`crates/lanekeep-cli/src/session.rs`), before it hands the held one back. It is the
246    /// difference between state that is *stale* and state that is *gone*: [`Self::revalidate`]
247    /// repairs the first, and nothing repairs the second.
248    ///
249    /// The `tsc` provider is the one that answers `true`: a breached `timeouts.analysis` kills
250    /// its sidecar, and nothing respawns it, so every later request in that session failed with
251    /// "the sidecar exited without answering" for the life of the editor — one slow build
252    /// bricking the session, where `lanekeep check` over the same project spawns a sidecar and
253    /// succeeds. A limit must cancel the run it breached and nothing after it.
254    ///
255    /// The default is `false`: a provider whose state is ordinary memory cannot be gone while
256    /// the process that holds it is running.
257    fn needs_rebuild(&self) -> bool {
258        false
259    }
260
261    /// Whether this provider spends the run's [`AnalysisBudget`].
262    ///
263    /// The engine reads the accumulator between one file and the next and cancels the run when
264    /// it is past `timeouts.analysis`; this is what says whether that question is worth asking
265    /// at all. It used to be asked of the *configuration* — `types.provider == 'tsc'` — which
266    /// is the wrong thing twice over: a session that hands the engine a provider of its own
267    /// (#191) is not described by the config it was built from, and a builtin run whose config
268    /// happened to say `tsc` would be bounded by a budget that names work it never does.
269    ///
270    /// The default is `false`, which is the builtin oracle's answer: its cost is rule
271    /// execution, and the run clock already bounds that.
272    fn spends_analysis_budget(&self) -> bool {
273        false
274    }
275}
276
277/// Why a provider could not do its work — preparing a run in [`TypeProvider::begin_run`], or
278/// answering a question afterwards, which [`TypeProvider::failure`] reports in the same shape.
279///
280/// The two arms are the engine's two exits: `Timeout` is `RunError::AnalysisTimeout`, which
281/// names `timeouts.analysis`, and `Failed` is `RunError::Provider`, which names the toolchain.
282/// Telling them apart matters because the remedies are different, and printing the wrong one
283/// is advice that cannot work.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum BeginRunError {
286    /// The provider's work outlived the analysis budget.
287    Timeout(String),
288    /// The provider could not do its work at all.
289    Failed(String),
290}
291
292#[cfg(test)]
293mod tests {
294    use std::sync::atomic::{AtomicUsize, Ordering};
295
296    use super::*;
297
298    /// A provider that answers nothing and counts revalidations.
299    ///
300    /// Nothing here needs a parsed tree: the property under test is that the call reaches an
301    /// implementor through the trait object a session holds, which is exactly what a
302    /// concrete-typed test could not establish.
303    #[derive(Default)]
304    struct Counting(AtomicUsize);
305
306    impl TypeProvider for Counting {
307        fn type_of(&self, _q: Query<'_>) -> Option<Type> {
308            None
309        }
310        fn symbol_of(&self, _q: Query<'_>) -> Option<Symbol> {
311            None
312        }
313        fn return_type_of(&self, _q: Query<'_>) -> Option<Type> {
314            None
315        }
316        fn is_assignable_to(&self, _q: Query<'_>, _module: &str, _name: &str) -> Option<bool> {
317            None
318        }
319        fn complete(&self, _q: Query<'_>) -> bool {
320            false
321        }
322        fn identity(&self) -> Vec<u8> {
323            Vec::new()
324        }
325        fn revalidate(&self, _files: &FileAccess) {
326            self.0.fetch_add(1, Ordering::Relaxed);
327        }
328    }
329
330    /// A provider that overrides nothing, to prove the default body exists.
331    #[derive(Default)]
332    struct Silent;
333
334    impl TypeProvider for Silent {
335        fn type_of(&self, _q: Query<'_>) -> Option<Type> {
336            None
337        }
338        fn symbol_of(&self, _q: Query<'_>) -> Option<Symbol> {
339            None
340        }
341        fn return_type_of(&self, _q: Query<'_>) -> Option<Type> {
342            None
343        }
344        fn is_assignable_to(&self, _q: Query<'_>, _module: &str, _name: &str) -> Option<bool> {
345            None
346        }
347        fn complete(&self, _q: Query<'_>) -> bool {
348            false
349        }
350        fn identity(&self) -> Vec<u8> {
351            Vec::new()
352        }
353    }
354
355    #[test]
356    fn revalidate_reaches_an_implementor_through_the_trait_object() {
357        let counting = std::sync::Arc::new(Counting::default());
358        let held: std::sync::Arc<dyn TypeProvider> = counting.clone();
359        let files = FileAccess::new(std::path::Path::new("."));
360
361        held.revalidate(&files);
362        held.revalidate(&files);
363
364        assert_eq!(
365            counting.0.load(Ordering::Relaxed),
366            2,
367            "a session revalidates once per request, through the trait object it holds"
368        );
369    }
370
371    #[test]
372    fn a_provider_that_holds_nothing_need_not_override_revalidate() {
373        // The default body is what keeps every other implementor compiling — a provider with
374        // no cache has nothing to drop, and requiring it to say so would be noise.
375        let held: std::sync::Arc<dyn TypeProvider> = std::sync::Arc::new(Silent);
376        held.revalidate(&FileAccess::new(std::path::Path::new(".")));
377    }
378}