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§
Sourcefn return_type_of(&self, q: Query<'_>) -> Option<Type>
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.
Sourcefn is_assignable_to(
&self,
q: Query<'_>,
module: &str,
name: &str,
) -> Option<bool>
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.
Sourcefn complete(&self, q: Query<'_>) -> bool
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.
Sourcefn identity(&self) -> Vec<u8> ⓘ
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§
Sourcefn dependency_paths(&self) -> BTreeSet<FilePath>
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.
Sourcefn revalidate(&self, files: &FileAccess)
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).
Sourcefn begin_run(
&self,
files: &dyn Fn() -> Vec<FilePath>,
budget: AnalysisBudget,
) -> Result<Vec<u8>, BeginRunError>
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.
Sourcefn failure(&self) -> Option<BeginRunError>
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.
Sourcefn notices(&self) -> Vec<String>
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.
Sourcefn needs_rebuild(&self) -> bool
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.
Sourcefn spends_analysis_budget(&self) -> bool
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 configuration — types.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§
impl TypeProvider for BuiltinProvider
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.