Skip to main content

bynk_check/
check_pipeline.rs

1//! The shared per-unit/per-file resolve-check core, used by both
2//! `bynk-emit`'s `check_unit_files` (`Mode::Build` and `Mode::Analyse`) and
3//! this crate's own [`crate::analysis::analyse_project`].
4//!
5//! P4.1 (#1115), the same `extract, don't duplicate` move as
6//! [`crate::project_model`]: `check_unit_files`'s per-file body is identical
7//! for both modes except for four `record_analyse_types` call sites (the
8//! error-path exits) and the final "Analyse mode always stops here, Build
9//! mode falls through to `certify`+`emit_unit`" branch. This module owns
10//! everything up to (not including) that branch — [`check_file_core`]
11//! returns `Some(TypedCommons)` only on the fully-clean, non-blocked path, so
12//! a caller that wants to emit knows exactly when it may. The four
13//! error-path recordings are unconditional here now (previously gated on
14//! `mode == Mode::Analyse`) — behaviour-preserving for the `Mode::Build`
15//! caller, which never took that branch anyway (`mode == Mode::Analyse`
16//! gated it), and whose `exprs` sink `compile_project`'s `ProjectOutput`
17//! never exposes.
18//!
19//! What stayed in `bynk-emit`: `Mode` itself (meaningless here — this
20//! crate's own entry point has exactly one behaviour), `certify`+`emit_unit`
21//! (real emission), and the decision of *whether* to record the clean-path
22//! types (each caller does that itself with the `Some(TypedCommons)` this
23//! module hands back — `bynk-emit`'s `Mode::Build` caller skips it,
24//! `Mode::Analyse` and this crate's own entry point both call
25//! [`record_analyse_types`]).
26
27use std::collections::{BTreeMap, HashMap, HashSet};
28use std::path::Path;
29use std::sync::Arc;
30
31use crate::checker::{self, TypedCommons, Types};
32use crate::context_checks::{
33    check_context_constraints, check_context_declarations, check_handler_constraints,
34};
35use crate::expr_types::ExprTypeSink;
36use crate::hints::HintSink;
37use crate::index::RefSink;
38use crate::locals::LocalsSink;
39use crate::project_model::{ErrorSink, UnitInfo};
40use crate::requirements::RequirementSink;
41use crate::resolver::{self, MethodTable as ResolverMethodTable, ResolvedCommons};
42use crate::symbols::{ConsumedType, UnitTable, build_cross_context_info, combined_types_for};
43use bynk_project::{ParsedFile, UnitKind};
44use bynk_syntax::ast::{CommonsItem, ExprId, FnName, TypeDecl};
45
46/// Record a file's (possibly partial) expression types into the Analyse-mode
47/// sink. Called at every per-file exit in the check loop so `.`-member
48/// completion and signature help get the receiver's type even when a later
49/// check phase errors for the file (ADR 0094). A no-op-shaped wrapper,
50/// factored out so the four error-path exits (now unconditional, see this
51/// module's own doc comment) and every clean-path caller share one call.
52pub fn record_analyse_types(
53    exprs: &mut ExprTypeSink,
54    source_path: &Path,
55    synthetic: bool,
56    types: &HashMap<ExprId, checker::TypedExpr>,
57) {
58    exprs.enter_file(source_path, synthetic);
59    exprs.record_file(types);
60}
61
62/// The four parallel per-project maps `build_cross_context_info`/
63/// `combined_types_for` need (their own general, map-based signature — see
64/// [`UnitCheckCtx`]'s own doc comment for why the per-file core materialises
65/// them from `unit_info` rather than changing that signature).
66type CrossContextViews = (
67    HashMap<String, UnitTable>,
68    HashMap<String, Vec<String>>,
69    HashMap<String, Vec<String>>,
70    HashMap<String, HashMap<String, String>>,
71);
72
73/// v0.29.4: `build_cross_context_info` (and its `combined_types_for` helper)
74/// is a general map-based function — the test-emission path calls it with
75/// *synthetic* harness maps, not `unit_info` — so it keeps its parallel-map
76/// signature. The per-file core only has `unit_info`, so this materialises
77/// the four views that one call needs, once per unit ahead of the file loop
78/// — but only for a context/adapter, `build_cross_context_info`'s only
79/// caller. `UnitTable` owns every declaration body in the unit, so for every
80/// other unit kind (including the seven injected first-party commons) this
81/// would otherwise be a whole-project deep clone, performed and discarded,
82/// once per unit.
83pub struct UnitCheckCtx {
84    cross_context_views: Option<CrossContextViews>,
85    /// #907: the exact set of type names `emit_context_rebrands` rebrands for
86    /// this unit — names brought in via `uses` of a *commons* specifically
87    /// (not a local declaration, and not a type surfaced via `consumes`,
88    /// which `imported_from_kind` tags `UnitKind::Context` in
89    /// `merge_consumed_exports` and which the emitter never rebrands).
90    pub uses_commons_type_names: HashSet<String>,
91    /// #1710: the declarations recovery skipped in the files this unit can see
92    /// (its own, and the units it `uses` and `consumes`). A reference to one is
93    /// a known name, not an unknown one (#1663's Decision B), so its
94    /// unknown-name echo is not reported. One level of `uses`, matching
95    /// `compose_unit_symbols`. A suite sees its target's skipped names because
96    /// it carries its target's unit name; it is in no unit's `uses`/`consumes`.
97    pub visible_broken_names: Vec<String>,
98    /// #1807: the types this unit's table carries only because an imported
99    /// declaration reaches them (`close_reachable_types`), each with its
100    /// owning commons. Resolvable, never nameable: the naming gate
101    /// (`resolver::check_hidden_type_names`) runs beside the resolver.
102    pub hidden_types: BTreeMap<String, String>,
103    /// #1824: the reached types this unit's own declarations shadow, which
104    /// `shadow_uses` checks the unit's bodies against.
105    pub shadowed: crate::project_model::ShadowedReaches,
106    /// S3 (#1963): the project's `[unstable]` policy, one per project.
107    pub policy: Arc<crate::stability::StabilityPolicy>,
108}
109
110/// Build the per-unit prelude [`check_file_core`] shares across every file
111/// in the unit — see [`UnitCheckCtx`]'s own doc comment.
112#[allow(clippy::too_many_arguments)]
113pub fn prepare_unit_check_ctx(
114    name: &str,
115    kind: UnitKind,
116    broken: &crate::project_model::BrokenDeclNames,
117    unit_info: &BTreeMap<String, UnitInfo>,
118    combined_types: &HashMap<String, Arc<TypeDecl>>,
119    imported_from_kind: &HashMap<String, UnitKind>,
120    reached_types: &crate::project_model::ReachedTypes,
121    policy: &Arc<crate::stability::StabilityPolicy>,
122) -> UnitCheckCtx {
123    let cross_context_views = if kind == UnitKind::Context || kind == UnitKind::Adapter {
124        let unit_tables: HashMap<String, UnitTable> = unit_info
125            .iter()
126            .map(|(n, i)| (n.clone(), i.table.clone()))
127            .collect();
128        let unit_uses: HashMap<String, Vec<String>> = unit_info
129            .iter()
130            .map(|(n, i)| (n.clone(), i.uses.clone()))
131            .collect();
132        let unit_consumes: HashMap<String, Vec<String>> = unit_info
133            .iter()
134            .map(|(n, i)| (n.clone(), i.consumes.clone()))
135            .collect();
136        let unit_consumes_aliases: HashMap<String, HashMap<String, String>> = unit_info
137            .iter()
138            .map(|(n, i)| (n.clone(), i.aliases.clone()))
139            .collect();
140        Some((unit_tables, unit_uses, unit_consumes, unit_consumes_aliases))
141    } else {
142        None
143    };
144    let uses_commons_type_names: HashSet<String> = imported_from_kind
145        .keys()
146        .filter(|n| {
147            crate::resolver::compute_is_uses_commons_type(imported_from_kind, combined_types, n)
148        })
149        .cloned()
150        .collect();
151    let visible_broken_names: Vec<String> = std::iter::once(name)
152        .chain(
153            unit_info
154                .get(name)
155                .into_iter()
156                .flat_map(|i| i.uses.iter().chain(i.consumes.iter()).map(String::as_str)),
157        )
158        .filter_map(|u| broken.get(u))
159        .flatten()
160        .cloned()
161        .collect();
162    UnitCheckCtx {
163        cross_context_views,
164        uses_commons_type_names,
165        visible_broken_names,
166        hidden_types: reached_types.hidden.clone(),
167        shadowed: reached_types.shadowed.clone(),
168        policy: policy.clone(),
169    }
170}
171
172/// The clean-path output of [`check_file_core`]: the typed, fully-checked
173/// unit plus the per-file cross-context info that produced it — a
174/// `Mode::Build` caller needs both to reach `certify`+`emit_unit` (`emit_unit`
175/// takes `cross_context_for_file` as its own argument, so this avoids making
176/// the caller recompute it from `ctx`/`unit_info` a second time).
177pub struct FileCheckResult {
178    pub typed: TypedCommons,
179    pub cross_context: resolver::CrossContextInfo,
180}
181
182/// The shared resolve+check+context-checks core for one file, factored out
183/// of `check_unit_files` (see this module's own doc comment). Returns
184/// `Some(FileCheckResult)` only on the fully-clean, non-blocked path — the
185/// signal a `Mode::Build` caller uses to know it may proceed to
186/// `certify`+`emit_unit`. Every error/blocked exit records best-effort
187/// partial types unconditionally (see [`record_analyse_types`]) and returns
188/// `None`.
189#[allow(clippy::too_many_arguments)]
190pub fn check_file_core(
191    name: &str,
192    kind: UnitKind,
193    pf: &ParsedFile,
194    unit_info: &BTreeMap<String, UnitInfo>,
195    combined_types: &HashMap<String, Arc<TypeDecl>>,
196    combined_fns: &HashMap<String, Arc<bynk_syntax::ast::FnDecl>>,
197    combined_methods: &HashMap<String, ResolverMethodTable>,
198    local_names: &HashSet<String>,
199    local_methods_for_type: &HashMap<String, Vec<bynk_syntax::ast::FnDecl>>,
200    consumed_types: &HashMap<String, ConsumedType>,
201    imported_from: &HashMap<String, String>,
202    ctx: &UnitCheckCtx,
203    errors: &mut ErrorSink,
204    refs: &mut RefSink,
205    hints: &mut HintSink,
206    locals: &mut LocalsSink,
207    exprs: &mut ExprTypeSink,
208    requirements: &mut RequirementSink,
209    tys: &Arc<Types>,
210) -> Option<FileCheckResult> {
211    let mut emit_items: Vec<CommonsItem> = Vec::new();
212    let types_in_this_file: HashSet<String> = pf
213        .items()
214        .iter()
215        .filter_map(|it| match it {
216            CommonsItem::Type(t) => Some(t.name.name.clone()),
217            // Events track, slice 0 (spine #936): an `event` shares the
218            // `types` namespace, so a multi-file context's method dispatch
219            // treats its name the same as a `type`'s.
220            CommonsItem::Event(e) => Some(e.name.name.clone()),
221            _ => None,
222        })
223        .collect();
224    for item in pf.items() {
225        match item {
226            CommonsItem::Type(t) => {
227                emit_items.push(CommonsItem::Type(t.clone()));
228            }
229            CommonsItem::Fn(f) => match &f.name {
230                FnName::Free(_) => emit_items.push(CommonsItem::Fn(f.clone())),
231                FnName::Method { type_name, .. } => {
232                    if types_in_this_file.contains(&type_name.name) {
233                        emit_items.push(CommonsItem::Fn(f.clone()));
234                    }
235                }
236            },
237            CommonsItem::Capability(c) => {
238                emit_items.push(CommonsItem::Capability(c.clone()));
239            }
240            CommonsItem::Provider(p) => {
241                emit_items.push(CommonsItem::Provider(p.clone()));
242            }
243            CommonsItem::Service(s) => {
244                emit_items.push(CommonsItem::Service(s.clone()));
245            }
246            CommonsItem::Agent(a) => {
247                emit_items.push(CommonsItem::Agent(a.clone()));
248            }
249            CommonsItem::Actor(a) => {
250                // Actors emit no standalone TS, but are carried so the
251                // emitter can read their schemes for the verification seam.
252                emit_items.push(CommonsItem::Actor(a.clone()));
253            }
254            CommonsItem::Messages(m) => {
255                emit_items.push(CommonsItem::Messages(m.clone()));
256            }
257            CommonsItem::Event(e) => {
258                emit_items.push(CommonsItem::Event(e.clone()));
259            }
260            CommonsItem::Asset(a) => {
261                emit_items.push(CommonsItem::Asset(a.clone()));
262            }
263            CommonsItem::Embed(e) => {
264                emit_items.push(CommonsItem::Embed(e.clone()));
265            }
266        }
267    }
268    for type_name in &types_in_this_file {
269        if let Some(methods) = local_methods_for_type.get(type_name) {
270            for m in methods {
271                let already = emit_items.iter().any(|it| match it {
272                    CommonsItem::Fn(existing) => match &existing.name {
273                        FnName::Method {
274                            type_name: t,
275                            method_name: n,
276                        } => match &m.name {
277                            FnName::Method {
278                                type_name: t2,
279                                method_name: n2,
280                            } => t.name == t2.name && n.name == n2.name,
281                            _ => false,
282                        },
283                        _ => false,
284                    },
285                    _ => false,
286                });
287                if !already {
288                    emit_items.push(CommonsItem::Fn(m.clone()));
289                }
290            }
291        }
292    }
293
294    // Synthesize a "Commons-shaped" view of this file's items so we can
295    // drive the existing resolver/checker without duplication.
296    let synthetic_commons = pf.as_synthetic_commons(emit_items);
297
298    // Cross-context info (v0.6) for contexts: consumed contexts, aliases,
299    // services, and types. Computed once below; reused for the resolver,
300    // checker, and (in `bynk-emit`) the emitter. v0.18: adapters get it too,
301    // so an external provider's `given` resolves against the adapter's
302    // flattened consumed capabilities (spec §4.5).
303    let cross_context_for_file =
304        if let Some((unit_tables, unit_uses, unit_consumes, unit_consumes_aliases)) =
305            &ctx.cross_context_views
306        {
307            let mut cci = build_cross_context_info(
308                name,
309                unit_consumes,
310                unit_consumes_aliases,
311                unit_uses,
312                unit_tables,
313            );
314            cci.flattened_caps = unit_info[name].flattened.clone();
315            cci
316        } else {
317            resolver::CrossContextInfo::default()
318        };
319
320    // Events slice 3a (#972): this unit's own local + direct-`uses` types
321    // (deliberately narrower than `combined_types`, which also merges
322    // `consumes`) — the same view `emit_consumed_context_helpers` (#973)
323    // builds for a *subscriber* regenerating this unit's own event codecs
324    // cross-context. `check_context_declarations` uses it to validate an
325    // event field default is constructible in that narrower view, not just
326    // this unit's own wider one.
327    let subscriber_visible_types: HashMap<String, Arc<TypeDecl>> =
328        if let Some((unit_tables, unit_uses, _, _)) = &ctx.cross_context_views {
329            combined_types_for(name, unit_tables, unit_uses)
330        } else {
331            HashMap::new()
332        };
333
334    // `ResolvedCommons::new` derives `local_type_names`/`event_type_names`
335    // from this unit's own pre-merge table (`unit_info[name].table`), not
336    // `combined_types` (already local+uses+consumes merged) — same
337    // distinction the caller's `local_names` exists for. `Events.emit[E]`
338    // additionally needs "is this specifically an event" on top of
339    // owner-only emission (an ordinary local type must not pass as an emit
340    // target just because it's locally declared), hence the separate
341    // `events` table. Both are empty for a unit absent from `unit_info`.
342    let empty_types = HashMap::new();
343    let empty_events = HashMap::new();
344    let local_table = unit_info.get(name).map(|i| &i.table);
345    let local_types = local_table.map(|t| &t.types).unwrap_or(&empty_types);
346    let local_events = local_table.map(|t| &t.events).unwrap_or(&empty_events);
347
348    let resolved = ResolvedCommons::new(
349        synthetic_commons,
350        combined_types.clone(),
351        local_types,
352        combined_fns.clone(),
353        combined_methods.clone(),
354        HashMap::new(),
355        local_events,
356        cross_context_for_file.clone(),
357        // ADR 0116 D6: provenance for the `bynk.list` deprecation lint.
358        imported_from.clone(),
359        kind == UnitKind::Context,
360        ctx.uses_commons_type_names.clone(),
361    )
362    // #1883, slice 1a: this unit's assets plus its `uses`d commons' ([DECISION G]).
363    .with_assets(crate::assets::compose_unit_assets(name, unit_info))
364    // #2387, slice 1: likewise this unit's embeds and its `uses`d commons'.
365    .with_embeds(crate::embeds::compose_unit_embeds(name, unit_info))
366    // #1925: imports hidden behind a local declaration of another kind.
367    .with_shadowed_imports(crate::project_model::shadowed_imports(name, unit_info))
368    // S3 (#1963): which names in scope are a first-party extensible enum,
369    // decided here, where the unit's full provenance is known.
370    .with_extensible_enums(
371        imported_from
372            .iter()
373            .filter(|(n, unit)| crate::firstparty::is_extensible(unit, n))
374            .map(|(n, _)| n.clone())
375            .collect(),
376    )
377    // S3 (#1963): the project's policy, and this unit's blocked imports. A
378    // first-party unit's own bodies are the library's business, never the
379    // project's opt-in, so they check under a policy that blocks nothing.
380    .with_stability(if pf.is_synthetic() {
381        crate::stability::UnitStability {
382            policy: Arc::new(crate::stability::StabilityPolicy::permissive()),
383            blocked_imports: HashMap::new(),
384        }
385    } else {
386        crate::stability::UnitStability {
387            policy: ctx.policy.clone(),
388            blocked_imports: ctx.policy.blocked_imports(imported_from),
389        }
390    });
391    refs.enter_file(&pf.identity_path(), name, pf.is_synthetic());
392    // v0.27: synthetic and test/integration files record no hints — neither
393    // surfaces in an editor (the `assemble_index` rule).
394    hints.enter_file(
395        &pf.identity_path(),
396        pf.is_synthetic() || matches!(pf.kind(), UnitKind::Test | UnitKind::Integration),
397    );
398    // v0.31: locals serve completion/navigation in test files too — only
399    // synthetic (toolchain-injected) files are muted.
400    locals.enter_file(&pf.identity_path(), pf.is_synthetic());
401    // v0.99: capability requirements follow the inlay-hint muting rule —
402    // synthetic and test/integration files surface none in an editor.
403    requirements.enter_file(
404        &pf.identity_path(),
405        pf.is_synthetic() || matches!(pf.kind(), UnitKind::Test | UnitKind::Integration),
406    );
407    // #1663 (Decision A): a resolve error no longer stops the file before the
408    // checker. Every declaration is still checked; the checker's diagnostics
409    // in a declaration the resolver rejected are its echoes and are dropped
410    // (`without_resolve_echoes`), and the file still fails here.
411    let mut resolve_errors = resolver::resolve_file_record(&resolved, refs).err();
412    // #1807: a hidden type written in this file is an unknown type, and is a
413    // resolve error like any other (Decision A drops the checker's echoes).
414    let hidden_named =
415        resolver::check_hidden_type_names(&resolved.commons.items, &ctx.hidden_types);
416    if !hidden_named.is_empty() {
417        resolve_errors
418            .get_or_insert_with(Vec::new)
419            .extend(hidden_named);
420    }
421    // #1927: a `uses`-imported type a local value hides may not be written
422    // either, and its errors are resolve errors the same way.
423    let shadowed_named = resolver::check_shadowed_type_names(
424        &resolved.commons.items,
425        &resolved.shadowed_imports,
426        &resolved.types,
427    );
428    if !shadowed_named.is_empty() {
429        resolve_errors
430            .get_or_insert_with(Vec::new)
431            .extend(shadowed_named);
432    }
433    let item_spans: Vec<bynk_syntax::span::Span> =
434        resolved.commons.items.iter().map(|i| i.span()).collect();
435    // The unit's own span is this file's, even when it has no items of its own
436    // (only methods on a sibling file's type, or only `uses`).
437    let own_file = resolved.commons.span.file;
438    let rc = checker::check_record_in(resolved, tys, refs, hints, locals, requirements);
439    if let Some(resolve_errors) = &resolve_errors {
440        // #1710: a reference to a declaration recovery skipped (here or in a
441        // file this unit can see) is a known name; its echo isn't reported. It
442        // still counts as a resolve error below (Decision A).
443        let (shown, _hidden) =
444            resolver::split_broken_decl_echoes(resolve_errors.clone(), &ctx.visible_broken_names);
445        errors.extend_for(Some(&pf.identity_path()), shown);
446    }
447    // Every diagnostic past the resolver goes out through this: the checker's
448    // errors and warnings, and the declaration stages' (`ours`, below).
449    // Decision A drops the checker's follow-ons inside a declaration the
450    // resolver rejected; #1710 then drops echoes of a declaration recovery
451    // skipped (a method, a capability, an actor, a consumed context's service)
452    // that this unit can see. The checker reports those under its own codes,
453    // in any unit kind, so the split runs here, not only in the stages.
454    let unechoed = |errs: Vec<bynk_syntax::CompileError>| {
455        let errs = match &resolve_errors {
456            Some(r) => resolver::without_resolve_echoes(errs, r, &item_spans),
457            None => errs,
458        };
459        resolver::split_broken_decl_echoes(errs, &ctx.visible_broken_names).0
460    };
461    // #1663: the declaration stages walk the whole unit's handlers, so a file's
462    // pass also meets another file's faults — and attributes them to this
463    // file, at a position in the wrong source. That file's own pass reports
464    // them; here, keep only this file's diagnostics (or unlocated ones), and
465    // decide whether a stage failed this file from those alone.
466    let ours = |errs: Vec<bynk_syntax::CompileError>| -> Vec<bynk_syntax::CompileError> {
467        unechoed(
468            errs.into_iter()
469                .filter(|e| e.span.file == own_file || e.span == bynk_syntax::span::Span::default())
470                .collect(),
471        )
472    };
473    // #1663: whether this file has already failed (a resolve or type error).
474    // The later stages still check its other declarations; the file returns
475    // no result at the end.
476    let mut failed = resolve_errors.is_some();
477    let typed = match rc.result {
478        Ok(t) => {
479            // v0.89 (ADR 0117): a unit that checks clean may still carry
480            // non-failing warnings — push them into the (severity-aware)
481            // sink, where they are classified as warnings and never gate.
482            if !t.warnings.is_empty() {
483                errors.extend_for(Some(&pf.identity_path()), unechoed(t.warnings.clone()));
484            }
485            t
486        }
487        Err(errs) => {
488            errors.extend_for(Some(&pf.identity_path()), unechoed(errs));
489            // #1663 (Decision A): a type error in one declaration must not
490            // hide the next stage's checks of the others (a service handler's
491            // body is typed in `check_context_declarations`).
492            failed = true;
493            // ADR 0094: the program as checked so far carries the
494            // best-effort types of every expression that did check. It is
495            // not recorded here: checking goes on, and whichever exit below
496            // the file reaches records it once, with the handler bodies
497            // `check_context_declarations` types in the meantime.
498            rc.typed_despite_errors?
499        }
500    };
501
502    // Run the context-specific checks: forbidden construction, private-type
503    // references.
504    if kind == UnitKind::Context {
505        let context_check_errs = ours(check_context_constraints(
506            &typed,
507            consumed_types,
508            local_names,
509            tys,
510        ));
511        if !context_check_errs.is_empty() {
512            errors.extend_for(Some(&pf.identity_path()), context_check_errs);
513            record_analyse_types(
514                exprs,
515                &pf.identity_path(),
516                pf.is_synthetic(),
517                &typed.expr_types,
518            );
519            return None;
520        }
521    }
522
523    // v0.5: check capability/provider/service/agent declarations. v0.18:
524    // adapters run these too — an external provider's `given` resolves
525    // through the same path as a bodied provider's (the service/agent
526    // checks are vacuous for adapters, which have none).
527    let mut typed = typed;
528    let unit_table_owned = unit_info.get(name).map(|i| i.table.clone());
529    if (kind == UnitKind::Context || kind == UnitKind::Adapter)
530        && let Some(table) = unit_table_owned.as_ref()
531    {
532        let decl_errs = ours(check_context_declarations(
533            &mut typed,
534            table,
535            &cross_context_for_file,
536            kind == UnitKind::Context,
537            &ctx.uses_commons_type_names,
538            &subscriber_visible_types,
539            refs,
540            hints,
541            locals,
542            requirements,
543            tys,
544        ));
545        if !decl_errs.is_empty() {
546            // ADR 0117: a warning-severity declaration diagnostic (e.g. the
547            // `@indexed` hygiene hints) must not block emission — only an
548            // error does. Partition first, then gate on error severity
549            // alone.
550            let blocks_emission = decl_errs.iter().any(|e| {
551                matches!(
552                    bynk_syntax::Severity::for_error(e),
553                    bynk_syntax::Severity::Error
554                )
555            });
556            errors.extend_for(Some(&pf.identity_path()), decl_errs);
557            if blocks_emission {
558                // ADR 0094: handler bodies are typed here — surface their
559                // best-effort types even when a declaration check (e.g. a
560                // service/agent wiring error) fails for the file.
561                record_analyse_types(
562                    exprs,
563                    &pf.identity_path(),
564                    pf.is_synthetic(),
565                    &typed.expr_types,
566                );
567                return None;
568            }
569            // Warnings only: the declarations are valid — fall through.
570        }
571    }
572
573    // #1700: the context constraints again, over the handler and provider
574    // bodies `check_context_declarations` has just typed.
575    if kind == UnitKind::Context {
576        let handler_errs = ours(check_handler_constraints(
577            &typed,
578            consumed_types,
579            local_names,
580            tys,
581        ));
582        if !handler_errs.is_empty() {
583            errors.extend_for(Some(&pf.identity_path()), handler_errs);
584            record_analyse_types(
585                exprs,
586                &pf.identity_path(),
587                pf.is_synthetic(),
588                &typed.expr_types,
589            );
590            return None;
591        }
592    }
593
594    // #1824: a value crossing between this unit's code and an imported
595    // position that names a type the unit shadows. Last, over every body the
596    // stages above have typed.
597    let shadow_errs = ours(crate::shadow_uses::check_shadowed_uses(
598        &typed,
599        &crate::shadow_uses::ShadowScope {
600            unit: name,
601            shadowed: &ctx.shadowed,
602            combined_types,
603            combined_fns,
604            combined_methods,
605            imported_from,
606        },
607    ));
608    if !shadow_errs.is_empty() {
609        errors.extend_for(Some(&pf.identity_path()), shadow_errs);
610        failed = true;
611    }
612
613    // #1663: every stage has now checked past the earlier errors; the file
614    // still fails on them.
615    if failed {
616        record_analyse_types(
617            exprs,
618            &pf.identity_path(),
619            pf.is_synthetic(),
620            &typed.expr_types,
621        );
622        return None;
623    }
624    Some(FileCheckResult {
625        typed,
626        cross_context: cross_context_for_file,
627    })
628}
629
630#[cfg(test)]
631mod tests {
632    use crate::testkit::analyse;
633
634    #[test]
635    fn a_file_with_a_type_error_still_records_its_other_expression_types() {
636        // ADR 0094: completion and signature help need a receiver's type even
637        // when another declaration in the same file fails to check.
638        let a = analyse(&[(
639            "demo.bynk",
640            "commons demo\n\nfn good(s: String) -> Int { s.length() }\n\nfn bad() -> Int { \"x\" }\n",
641        )]);
642        assert!(
643            !a.error_categories().is_empty(),
644            "the fixture must fail to check"
645        );
646        assert_eq!(a.type_at("demo.bynk", "s.length()"), "Int");
647    }
648
649    #[test]
650    fn a_call_to_a_declaration_that_failed_to_parse_is_not_an_unknown_name() {
651        // #1710: a broken declaration stays a known name, so its callers report
652        // nothing extra; only the parse error itself is reported.
653        let a = analyse(&[(
654            "demo.bynk",
655            "commons demo\n\nfn broken() -> Int { 1 + }\n\nfn user() -> Int { broken() }\n",
656        )]);
657        assert!(
658            !a.error_categories().is_empty(),
659            "the parse error is reported"
660        );
661        assert!(
662            !a.categories().contains(&"bynk.resolve.unknown_function"),
663            "a broken declaration's caller must not report it unknown:\n{}",
664            a.render()
665        );
666    }
667
668    #[test]
669    fn a_clean_file_s_types_are_recorded_on_the_clean_path() {
670        let a = analyse(&[(
671            "demo.bynk",
672            "commons demo\n\nfn f(s: String) -> Int { s.length() }\n",
673        )]);
674        a.assert_clean();
675        assert_eq!(a.type_at("demo.bynk", "s.length()"), "Int");
676    }
677}