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