Skip to main content

TypeProvider

Trait TypeProvider 

Source
pub trait TypeProvider: Send + Sync {
Show 13 methods // Required methods fn type_of(&self, q: Query<'_>) -> Option<Type>; fn symbol_of(&self, q: Query<'_>) -> Option<Symbol>; fn return_type_of(&self, q: Query<'_>) -> Option<Type>; fn is_assignable_to( &self, q: Query<'_>, module: &str, name: &str, ) -> Option<bool>; fn complete(&self, q: Query<'_>) -> bool; fn identity(&self) -> Vec<u8> ; // Provided methods fn dependency_paths(&self) -> BTreeSet<FilePath> { ... } fn revalidate(&self, files: &FileAccess) { ... } fn begin_run( &self, files: &dyn Fn() -> Vec<FilePath>, budget: AnalysisBudget, ) -> Result<Vec<u8>, BeginRunError> { ... } fn failure(&self) -> Option<BeginRunError> { ... } fn notices(&self) -> Vec<String> { ... } fn needs_rebuild(&self) -> bool { ... } fn spends_analysis_budget(&self) -> bool { ... }
}
Expand description

What answers ctx.types.

One implementation per strategy — the bounded builtin oracle here, a tsc sidecar in A3 — and the engine holds exactly one for a run. Self::identity is why the two cannot be confused by a cache: it is a key input, so a result computed by one is never served to a run using the other.

Required Methods§

Source

fn type_of(&self, q: Query<'_>) -> Option<Type>

The type of the expression at q.node.

Source

fn symbol_of(&self, q: Query<'_>) -> Option<Symbol>

Where the name at q.node came from.

Source

fn return_type_of(&self, q: Query<'_>) -> Option<Type>

What calling the function at q.node yields.

Separate from Self::type_of because a function declaration is not an expression, and giving type_of a signature type would invent a variant every rule would then have to unpack. Accepts a call expression, a function-like declaration, or an identifier bound to one.

A generic call’s return may or may not be instantiated from its arguments, depending on the provider: the tsc provider answers the instantiated type — useMemo(() => 0n, []) is bigint — while the builtin oracle answers None, doing no call-site inference. The common divergence is exactly that: tsc names a type where the builtin is silent, and silence is spelled None, the “could not be sure” value every rule already handles. The two can also give different present answers for an overloaded call — the builtin reads the first declared overload, tsc the one the arguments select — a narrow case no shipped rule asks about, and one where tsc’s answer is the more precise of the two.

Source

fn is_assignable_to( &self, q: Query<'_>, module: &str, name: &str, ) -> Option<bool>

Whether the type at q.node is the type module exports as name, or declares a relationship to it — extends or implements — across files, through aliases of the named type.

Nominal, never structural. Some(false) is a real answer — the walk completed and found nothing — and None is “a link in the chain could not be read”, which a rule must not treat as a negative. A union is assignable only when every member is.

Three narrowings of what “found nothing” honestly covers: declaration merging is not followed, so when a name is declared more than once at a file’s top level only the first declaration is consulted; a generic annotation at the use site (let x: Box<number>) answers None, since type arguments are not read; and a name the named module does not export answers None rather than Some(false), because Some(false) there would make a requirement rule report on every value the module never claimed to type.

One documented gap: declarations are looked up at a file’s top level only, so a declaration that shadows the target’s name inside a function body is not distinguished from the top-level one with that name — the walk answers as if the shadow were the top-level declaration.

Source

fn complete(&self, q: Query<'_>) -> bool

Whether every import in q’s file resolved to something this provider could read.

false is the honest label on a partial answer: a rule that reports on an absent type would accuse code the provider never saw. Takes a whole Query rather than a path because the question is about the file’s imports, which cannot be enumerated without its tree.

The verdict is about reading, per declaration where one is reached. A parse fault anywhere in a resolved declaration file used to mark every importer of it incomplete — one unparsed construct in a fifty-thousand-line @types bundle, poisoning the whole project. The builtin provider judges a named import by the node its name reaches: a fault inside that node — an ERROR, or the MISSING token an unclosed brace leaves — makes the file incomplete, one elsewhere does not, and the declarations outside the damaged span answer normally. A name whose module resolved and parsed cleanly but which the provider cannot follow to a declaration is not incomplete: complete says whether every import was read, never whether every name is typeable, and export = X beside declare namespace X is a module read whole whose members answer undefined. A nameless import — side-effect, namespace, export * — has no single node to reach and keeps the whole-file verdict. Silence is still the safe direction: a link that cannot be read, wherever it sits, is false rather than a guess. A provider that delegates to tsc answers whatever its driver reports for the file as a whole.

Source

fn identity(&self) -> Vec<u8>

What this provider is, for the cache key.

Folded into analysis_hash by the engine. Two providers that would answer differently must return different bytes, and a provider whose own code changed must too — the builtin one derives it from oracle_identity, which digests this crate’s src/, for exactly that reason.

Provided Methods§

Source

fn dependency_paths(&self) -> BTreeSet<FilePath>

Every path this provider’s own dependency mechanism named for the run it last prepared.

--watch‘s allowlist unions this with Outcome::dependency_paths, because the two cover different providers’ dependencies and neither can stand in for the other. A provider whose reads travel through Query::files — the builtin oracle — already lands in Outcome::dependency_paths as per-file tracked reads, so its default here is empty rather than a duplicate of what the engine already collects. The tsc provider overrides it: the compiler reads through its own host, never through Query::files, so nothing it consults is a tracked read on any file, and without this override an edit to a linked package’s declaration file would wake --watch for nothing at all.

Answered after Self::begin_run, from whatever it last built — a provider held across runs (plan 6) reports the run it most recently prepared, not the run before it.

The default is empty, matching every provider that has no dependency mechanism outside tracked reads.

Source

fn revalidate(&self, files: &FileAccess)

Drop whatever this provider holds that the files no longer support.

Called once per request by a session that holds this provider across several of them (crates/lanekeep-cli/src/session.rs). A one-shot run builds a provider and throws it away, so for that path this is a no-op that costs a virtual call.

The default body is empty because a provider with no cache has nothing to revalidate, and requiring every implementor to write that down would be noise. The two that do hold something override it: the builtin provider re-hashes each declaration it parsed and forgets its memoized completeness, and the tsc provider does nothing here because begin_run already re-answers programs on every prepare, held provider or not. Nothing here reads an mtime — held state is keyed by content hash, which is the only reason it is allowed to be held at all (architecture §8.2).

Source

fn begin_run( &self, files: &dyn Fn() -> Vec<FilePath>, budget: AnalysisBudget, ) -> Result<Vec<u8>, BeginRunError>

Prepare for a run over the corpus files yields, and answer the per-run key term.

Called by the engine once per run, after discovery and before the run key is computed, for a fresh provider and a held one alike (plan 6). The default is nothing to build and no term: this provider’s dependencies are tracked reads on each entry. The tsc provider (plan 5) overrides it to build every program the run’s files belong to and answer the hash over their file lists and contents — that provider’s whole dependency mechanism (spec §5.6) — which is why the term is asked for through the trait rather than read off a concrete type the engine would have to downcast to. The run’s AnalysisBudget is the second parameter: a provider that spends wall clock time preparing must spend the run’s budget rather than one of its own, so a breach here is the same breach timeouts.analysis names everywhere else. The default body ignores it for the same reason it ignores the file list — it does no work.

The list is a closure, and the default body never calls it. Producing it costs the engine a second walk of the whole project, on the warm path, for a provider that may have no use for it — which is what the only provider that ships today does. A &[…] parameter makes that walk unconditional; a &dyn Fn makes it the caller’s cost only when a provider asks.

What the closure yields is the corpus as discovered, not the run’s --since or --staged selection: a program is a property of the project, and building one from a changed-files subset would answer a different question on a warm run than on a cold one.

§Errors

BeginRunError::Timeout when the work outlived its budget and BeginRunError::Failed for anything else. Both cancel the run, and the engine takes a different exit for each: RunError::AnalysisTimeout, which names timeouts.analysis, and RunError::Provider, which names the toolchain.

Source

fn failure(&self) -> Option<BeginRunError>

The first error this provider produced, if it has produced one.

Why this is on the trait at all. Every arm above answers “I don’t know” — None, or false from Self::complete — when the provider is broken, because the trait has nowhere to put an error. So a killed sidecar is indistinguishable, at this boundary, from a compiler that simply has no type for that node: the file finishes degraded and is committed under a valid cache key, which is a limit degrading a run instead of cancelling it. This is the one way a caller can tell the two apart, so the engine asks after every file and cancels the run when it is Some.

It is answered through the trait rather than off a concrete type because the engine holds an Arc<dyn TypeProvider> — a session’s held provider (plan 6) included — and the alternative is a downcast, which is the door plan 4 closed for the run key.

BeginRunError rather than an enum of its own: its two arms are exactly the two exits the engine has for a provider — a spent timeouts.analysis, and everything else — and a second type with the same two arms would be two spellings of one decision.

The default is None: a provider with no out-of-band state has nothing to report, and the builtin one has no analogous failure.

Source

fn notices(&self) -> Vec<String>

Lines this provider wants said about the run it has just prepared.

Asked once, after Self::begin_run, and printed by the CLI on stderr — never on stdout, which carries a run’s report and is what a machine reads. The engine prints nothing itself; it exposes these and the caller decides.

What they are for is a decision a provider made silently that changes its answers. The tsc provider’s is the ad-hoc program: a file no tsconfig.json under the project root claims is typed with this driver’s own options rather than the project’s, so strict is off for it and the same file checked from one directory up answers differently. That is not a failure — nothing is wrong and the run is correct — so it cannot be an error, and it is not nothing either.

The default is empty: a provider that made no such decision has nothing to say, and a notice printed by every run is noise rather than information.

Source

fn needs_rebuild(&self) -> bool

Whether this provider is past repair and has to be built again.

Asked by a session that holds a provider across requests (crates/lanekeep-cli/src/session.rs), before it hands the held one back. It is the difference between state that is stale and state that is gone: Self::revalidate repairs the first, and nothing repairs the second.

The tsc provider is the one that answers true: a breached timeouts.analysis kills its sidecar, and nothing respawns it, so every later request in that session failed with “the sidecar exited without answering” for the life of the editor — one slow build bricking the session, where lanekeep check over the same project spawns a sidecar and succeeds. A limit must cancel the run it breached and nothing after it.

The default is false: a provider whose state is ordinary memory cannot be gone while the process that holds it is running.

Source

fn spends_analysis_budget(&self) -> bool

Whether this provider spends the run’s AnalysisBudget.

The engine reads the accumulator between one file and the next and cancels the run when it is past timeouts.analysis; this is what says whether that question is worth asking at all. It used to be asked of the configurationtypes.provider == 'tsc' — which is the wrong thing twice over: a session that hands the engine a provider of its own (#191) is not described by the config it was built from, and a builtin run whose config happened to say tsc would be bounded by a budget that names work it never does.

The default is false, which is the builtin oracle’s answer: its cost is rule execution, and the run clock already bounds that.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§

Source§

impl TypeProvider for BuiltinProvider

Source§

impl TypeProvider for TscProvider

A program’s own reads reach the key through TypeProvider::begin_run’s listing, folded before any answer exists. A query’s reads — a resolution made outside any program’s host, answering a single question — are the other half: TscProvider::ask_recording records each through the asking Query’s own FileAccess, as an ordinary per-entry tracked read.