feat(source): expose borrowed read-only mixed source eight validation

This commit is contained in:
2026-09-24 21:20:11 -07:00
parent 7b06b8ea1b
commit 042e25efc2
12 changed files with 314 additions and 44 deletions
+4 -31
View File
@@ -3,10 +3,9 @@ mod raw_tokens;
#[path = "language/semantic_source.rs"]
mod semantic_source;
mod source3_bridge;
// Staged typed analysis only. Public mixed source8 admission waits for complete
// declaration lowering, source updates and the root-owned resource gate.
#[cfg(test)]
// Mixed source8 is exposed only through the separate borrowed read-only checker.
mod analysis_source;
pub use analysis_source::{CheckedMixed, check_mixed_bundle};
#[path = "migration_v2/source.rs"]
mod v2_source;
use crate::*;
@@ -480,7 +479,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
cursor: 0,
value_depth: 0,
limits,
#[cfg(test)]
mixed: None,
}
.document()
@@ -916,7 +914,6 @@ pub(crate) fn parse_unit(
cursor: 0,
value_depth: 0,
limits,
#[cfg(test)]
mixed: None,
}
.unit(entry, true)
@@ -1215,12 +1212,10 @@ struct Parser<'a> {
cursor: usize,
value_depth: usize,
limits: ParseLimits,
#[cfg(test)]
mixed: Option<analysis_source::mixed::State<'a>>,
}
impl Parser<'_> {
fn token(&self, index: usize) -> Option<Lexeme> {
#[cfg(test)]
if let Some(mixed) = &self.mixed {
return mixed.token(index);
}
@@ -1236,7 +1231,6 @@ impl Parser<'_> {
})
}
fn syntax_entry(&mut self) -> Result<(), Error> {
#[cfg(test)]
{
let span = self.current().span;
if let Some(mixed) = &mut self.mixed {
@@ -1266,9 +1260,7 @@ impl Parser<'_> {
if self.current().class != TokenClass::String {
return Err(self.error("Expected a double-quoted string."));
}
#[cfg(test)]
let span = self.current().span;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
let raw = &self.source[span.start..span.end];
let value = mixed.quoted_text(raw, span)?;
@@ -1290,9 +1282,8 @@ impl Parser<'_> {
}
fn optional_id(&mut self) -> Result<Id, Error> {
if self.text() != "id" {
#[cfg(test)]
if self.mixed.is_some() {
return Err(self.error("Private mixed syntax requires explicit IDs."));
return Err(self.error("Mixed source8 requires explicit UUIDs."));
}
return Ok(Id::new_v4());
}
@@ -1321,21 +1312,15 @@ impl Parser<'_> {
Ok(id)
}
fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> {
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.own_text(bytes, span)?;
}
#[cfg(not(test))]
let _ = (bytes, span);
Ok(())
}
fn release_text(&mut self, bytes: usize) {
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.release_text(bytes);
}
#[cfg(not(test))]
let _ = bytes;
}
fn fields(&mut self) -> Result<BTreeMap<String, Ast>, Error> {
self.take("{")?;
@@ -1383,7 +1368,6 @@ impl Parser<'_> {
.span
.end;
self.take(";")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.projection(1, Span { start, end })?;
}
@@ -1412,7 +1396,6 @@ impl Parser<'_> {
self.value_depth += 1;
let result = self.value_inner();
self.value_depth -= 1;
#[cfg(test)]
if let Ok(ast) = &result {
let end = self
.token(self.cursor - 1)
@@ -1433,7 +1416,6 @@ impl Parser<'_> {
fn value_inner(&mut self) -> Result<Ast, Error> {
let token = self.current();
let offset = token.span.start;
#[cfg(test)]
if let Some(mixed) = &self.mixed
&& (token.class == TokenClass::Number || matches!(self.text(), "-" | "+"))
{
@@ -1541,17 +1523,14 @@ impl Parser<'_> {
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
let mut header = if entry {
self.take("arcadia")?;
#[cfg(test)]
let mixed_mode = self.mixed.is_some();
#[cfg(not(test))]
let mixed_mode = false;
if if mixed_mode {
self.text() != "8"
} else {
!matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8")
} {
return Err(self.error(if mixed_mode {
"Private mixed syntax requires arcadia 8."
"Mixed source requires arcadia 8."
} else {
"Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8."
}));
@@ -1561,7 +1540,6 @@ impl Parser<'_> {
self.take(";")?;
let start = self.current().span.start;
self.take("project")?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.record(false, Span { start, end: start })?;
}
@@ -1606,7 +1584,6 @@ impl Parser<'_> {
let mut open: Vec<usize> = Vec::new();
loop {
let offset = self.current().span.start;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed
&& mixed.typed_header(self.cursor, open.is_empty())
{
@@ -1658,7 +1635,6 @@ impl Parser<'_> {
if !includes_allowed {
return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
}
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.include(Span {
start: offset,
@@ -1705,7 +1681,6 @@ impl Parser<'_> {
header = Some(entry_header);
continue;
}
#[cfg(test)]
if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) {
return Err(self.error("Use the short source8 analysis declaration word."));
}
@@ -1713,7 +1688,6 @@ impl Parser<'_> {
let word = self.current().text(self.source);
let kind = declaration_kind(word)
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
mixed.record(
matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)),
@@ -1750,7 +1724,6 @@ impl Parser<'_> {
let body_start = self.current().span.end;
self.take("{")?;
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
#[cfg(test)]
if let Some(mixed) = &mut self.mixed {
let span = Span {
start: offset,
@@ -1,14 +1,16 @@
//! Unresolved typed source8 syntax, staged for the complete source adapter.
//! This module does not admit a Project or enable public source8.
//! Private typed syntax and complete read-only mixed source8 resolution.
mod budget;
mod cursor;
mod declarations;
#[cfg(test)]
mod index;
mod lex;
mod lower;
pub(super) mod mixed;
mod syntax;
#[cfg(test)]
mod update;
#[cfg(test)]
mod write;
#[cfg(test)]
@@ -18,6 +20,7 @@ use super::Span;
use crate::analysis::{BinaryOp, UnaryOp};
use crate::analysis_numeric::Exact;
use budget::Budget;
pub use mixed::project::{CheckedMixed, check_mixed_bundle};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
@@ -104,6 +107,7 @@ pub enum Binding {
Expression(Expression),
}
#[cfg(test)]
pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
let tokens = lex::tokens(source, budget)?;
let mut cursor = cursor::Cursor::new(source, &tokens, budget);
@@ -294,6 +294,7 @@ impl Budget {
}
}
}
#[cfg(test)]
pub fn numeric_units(&self) -> u64 {
self.numeric.usage().numeric_units
}
@@ -42,6 +42,7 @@ impl Cursor<'_, '_> {
self.cursor += 1;
Ok(span)
}
#[cfg(test)]
pub(super) fn end(&self) -> Result<(), Error> {
if self.cursor != self.tokens.len() {
Err(error(
@@ -2,12 +2,15 @@
//! single complete-Project semantic admission after merging these additions.
use super::declarations::{self as d, Declaration, DeclarationKind};
use super::*;
#[cfg(test)]
use crate::Project;
use crate::analysis as a;
use crate::analysis_numeric::Dimension;
use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody};
use crate::{Element, ElementBody, Id, Kind, Layer, SharedBody};
use std::collections::{BTreeMap, BTreeSet};
/// Caller-assigned file key; spans are original offsets within that file.
#[cfg(test)]
pub(super) struct Unit {
pub file: usize,
pub declarations: Vec<Declaration>,
@@ -180,6 +183,7 @@ impl Tables {
Ok(())
}
#[cfg(test)]
fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> {
let at = Span::default();
self.insert(project.id, &project.alias, Category::Project, at, budget)?;
@@ -270,6 +274,7 @@ impl Tables {
/// Inputs must come from the same live Budget and stabilization pass. No scan,
/// exact conversion or reference charge occurs here. The ordinary skeleton is
/// not semantically admitted: its typed forward references may need additions.
#[cfg(test)]
pub(super) fn lower(
ordinary: &Project,
units: &[Unit],
@@ -13,7 +13,7 @@
//! value-end entries, projected value/field/header/declaration positions, lexical
//! owner slots and the file table. Checks do not recharge syntax or latch errors;
//! every failure must return immediately. Tokens themselves use the token bound.
//! This deliberately bounded staging policy is not calibrated public source8.
//! The public read checker selects both producers; editing remains separately staged.
use super::{
budget::{Budget, Limits},
@@ -53,11 +53,15 @@ pub(super) struct ParsedMixedSyntax<'s> {
struct FileSyntax<'s> {
path: &'s str,
source: &'s str,
// Retained original indexing metadata; the read result preserves its accounted ownership.
#[cfg_attr(not(test), allow(dead_code))]
tokens: Vec<lex::Token>,
ordinary: SourceUnit,
typed: Vec<declarations::Declaration>,
positions: Vec<(Id, EditDeclaration)>,
// A lexical owner is an occurrence index, not only a possibly repeated UUID.
// Retained original indexing metadata; the read result preserves its accounted ownership.
#[cfg_attr(not(test), allow(dead_code))]
lexical_owners: Vec<Option<usize>>,
}
@@ -256,7 +260,7 @@ impl State<'_> {
return Err(Error {
code: "invalid_source",
offset: declaration.span.start,
message: "Private mixed syntax requires explicit IDs.".into(),
message: "Mixed source8 requires explicit UUIDs.".into(),
});
}
self.typed.push(declaration);
@@ -276,17 +280,22 @@ pub(in crate::language) fn long_analysis_word(word: &str) -> bool {
)
}
#[cfg(test)]
pub(super) fn parse(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let profile = Limits::source8(ParseLimits {
let profile = source_profile(limits);
parse_with_profile(bundle, limits, profile)
}
fn source_profile(limits: ProjectLimits) -> Limits {
Limits::source8(ParseLimits {
// Storage is not the caller's final model record/ordered-step accounting.
max_source_bytes: limits.model.max_source_bytes,
max_step_references: limits.model.max_step_references,
..ParseLimits::UNLIMITED
});
parse_with_profile(bundle, limits, profile)
})
}
fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
@@ -297,6 +306,7 @@ fn located(path: &str, source: &str, error: Error) -> Vec<SourceDiagnostic> {
}]
}
#[cfg(test)]
fn parse_with_profile(
bundle: &SourceBundle,
limits: ProjectLimits,
@@ -2,6 +2,7 @@
//! Checks existing logical phase totals; does not meter every parser allocation.
use super::*;
use project::accounting::{Ledger, Slots};
#[cfg(test)]
use project::ordinary_index::rename::emission::PendingRename;
struct SnapshotPolicy {
@@ -10,17 +11,20 @@ struct SnapshotPolicy {
}
/// The allocation-owning bundle guard. No production constructor exists yet.
#[cfg(test)]
struct RetainedSnapshot {
bundle: SourceBundle,
policy: SnapshotPolicy,
retained: Slots,
}
enum SnapshotBacking<'s> {
#[cfg(test)]
Pending(&'s PendingRename<'s>),
Borrowed {
bundle: &'s SourceBundle,
policy: SnapshotPolicy,
},
#[cfg(test)]
Retained(&'s RetainedSnapshot),
}
@@ -29,8 +33,12 @@ pub(super) struct CarriedInput<'s> {
backing: SnapshotBacking<'s>,
}
enum GuardBacking<'s> {
#[cfg(test)]
Pending(&'s PendingRename<'s>),
Borrowed,
Borrowed {
_bundle: &'s SourceBundle,
},
#[cfg(test)]
Retained(&'s RetainedSnapshot),
}
@@ -40,6 +48,18 @@ pub(super) struct CarriedGuard<'s> {
backing: GuardBacking<'s>,
}
impl<'s> CarriedInput<'s> {
fn fresh(bundle: &'s SourceBundle, limits: ProjectLimits) -> Self {
Self {
backing: SnapshotBacking::Borrowed {
bundle,
policy: SnapshotPolicy {
limits: source_profile(limits),
project_limits: limits,
},
},
}
}
#[cfg(test)]
pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self {
Self {
backing: SnapshotBacking::Pending(pending),
@@ -47,6 +67,7 @@ impl<'s> CarriedInput<'s> {
}
fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) {
match self.backing {
#[cfg(test)]
SnapshotBacking::Pending(pending) => {
let (limits, project_limits) = pending.carried_policy();
(
@@ -64,9 +85,10 @@ impl<'s> CarriedInput<'s> {
bundle,
policy,
CarriedGuard {
backing: GuardBacking::Borrowed,
backing: GuardBacking::Borrowed { _bundle: bundle },
},
),
#[cfg(test)]
SnapshotBacking::Retained(owner) => (
&owner.bundle,
SnapshotPolicy {
@@ -83,8 +105,10 @@ impl<'s> CarriedInput<'s> {
impl CarriedGuard<'_> {
pub(super) fn slots(&self) -> Slots {
match &self.backing {
#[cfg(test)]
GuardBacking::Pending(pending) => pending.carried_slots(),
GuardBacking::Borrowed => Slots::default(),
GuardBacking::Borrowed { .. } => Slots::default(),
#[cfg(test)]
GuardBacking::Retained(owner) => owner.retained,
}
}
@@ -105,6 +129,7 @@ impl CarriedGuard<'_> {
}
/// Input already binds the actual bundle; callers cannot supply a replacement.
#[cfg(test)]
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
let (bundle, policy, guard) = input.into_parts();
parse_impl(
@@ -205,3 +230,12 @@ mod text_producer;
#[cfg(test)]
mod entry_producer;
pub(super) fn parse_fresh(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>> {
parse_entry_producers(CarriedInput::fresh(bundle, limits))
}
#[cfg(test)]
mod public_read;
@@ -0,0 +1,65 @@
//! Real public-constructor wiring; arbitrary fixture profiles are not public policy.
use super::*;
#[test]
fn public_read_fresh_constructor_keeps_zero_carry_both_modes_and_exact_policy() {
let bundle = SourceBundle {
entry: "main.arcadia".into(),
files: [("main.arcadia".into(), "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n".into())].into(),
};
let original = bundle.clone();
for (source_bytes, references, expected_bytes, expected_tokens, expected_references) in [
(4096, 17, 4096, 4096, 17),
(usize::MAX, usize::MAX, 64 * 1024 * 1024, 1_048_576, 500_000),
] {
let limits = ProjectLimits {
max_files: 7,
max_include_depth: 9,
max_include_edges: 11,
model: ParseLimits {
max_source_bytes: source_bytes,
max_graph_objects: 3,
max_records: 1,
max_step_references: references,
},
};
// This is the same production helper the exported checker calls.
let syntax = parse_fresh(&bundle, limits).unwrap();
let guard = syntax.carried.as_ref().expect("explicit carried mode");
assert!(matches!(guard.backing, GuardBacking::Borrowed { .. }));
assert_eq!(guard.slots(), Slots::default());
assert_eq!(syntax.budget.producer_text(), Some((8, 0)));
assert_eq!(syntax.budget.producer_entries(), Some(11));
assert_eq!(syntax.metadata, 11);
let profile = syntax.budget.limits();
assert_eq!(profile.bytes, expected_bytes);
assert_eq!(profile.tokens, expected_tokens);
assert_eq!(profile.entries, 262_144);
assert_eq!(profile.references, expected_references);
assert_eq!(profile.frames, 256);
assert_eq!(profile.numeric_units, 50_000_000);
assert_eq!(syntax.limits.max_files, 7);
assert_eq!(syntax.limits.max_include_depth, 9);
assert_eq!(syntax.limits.max_include_edges, 11);
assert_eq!(syntax.limits.model.max_source_bytes, source_bytes);
assert_eq!(syntax.limits.model.max_graph_objects, 3);
assert_eq!(syntax.limits.model.max_records, 1);
assert_eq!(syntax.limits.model.max_step_references, references);
let prior = parse(&bundle, limits).unwrap();
assert_eq!(syntax.budget.usage, prior.budget.usage);
assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units());
let resolved = project::resolve(syntax).unwrap();
let prior = project::resolve(prior).unwrap();
let checked = project::check_mixed_bundle(&bundle, limits).unwrap();
let mut expected = crate::Project::empty();
expected.id = Id::from_u128(1);
expected.alias = "p".into();
expected.name = "Project".into();
assert_eq!(resolved.project(), &expected);
assert_eq!(prior.project(), &expected);
assert_eq!(checked.project(), &expected);
assert!(checked.warnings().is_empty());
assert_eq!(resolved.warnings(), prior.warnings());
}
assert_eq!(bundle, original);
}
@@ -1,5 +1,5 @@
//! Private complete-Project admission from the actual complete syntax witness.
//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy.
//! Complete-Project admission and opaque public read-only mixed source8 witness.
//! The separate typed index/edit chain remains test-only; no replacement policy.
use super::super::{
Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span,
declarations as d, error, lower,
@@ -22,17 +22,108 @@ pub(super) fn text_producer_inventory(syntax: &ParsedMixedSyntax<'_>) -> Result<
accounting::syntax(syntax, &mut ledger)?;
Ok(ledger.live)
}
#[cfg(test)]
pub(in crate::language::analysis_source) mod ordinary_index;
/// An admitted mixed source8 model borrowing its complete original source bundle.
///
/// This non-Clone result grants immutable validation queries only. It is not an
/// editable source index, stabilized output, or permission to format/save a typed
/// model through the ordinary source APIs. All input UUIDs must be explicit.
///
/// The source cannot be mutated while a later query uses the result:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let mut bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// bundle.files.clear();
/// assert_eq!(checked.project().alias, "p");
/// ```
///
/// Nor can the result outlive that source:
///
/// ```compile_fail
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let checked = {
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap()
/// };
/// assert_eq!(checked.project().alias, "p");
/// ```
pub struct CheckedMixed<'s> {
resolved: ResolvedMixedProject<'s>,
}
impl CheckedMixed<'_> {
/// The complete admitted model, without mutation or ownership extraction.
pub fn project(&self) -> &Project {
self.resolved.project()
}
/// Actual located non-error validation diagnostics in validation order.
pub fn warnings(&self) -> &[SourceDiagnostic] {
self.resolved.warnings()
}
}
/// Validate a complete mixed ordinary/typed source8 bundle without changing bytes.
///
/// Every project, ordinary/typed declaration and typed parameter requires an
/// explicit UUID. Supplied files must be canonical and reachable from the entry.
/// Errors return no partial admitted model. There is no migration, ID generation,
/// filesystem discovery, formatting or editable-index construction.
///
/// Caller policy is applied across the entire bundle with the existing bounded
/// source8 syntax/storage/numeric policy; this is not an all-heap memory bound.
/// Ordinary parse/compile/edit/save APIs and CLI discovery remain separate.
///
/// ```
/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}};
/// let bundle = SourceBundle {
/// entry: "main.arcadia".into(),
/// files: [("main.arcadia".into(),
/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(),
/// };
/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap();
/// assert_eq!(checked.project().alias, "p");
/// assert!(checked.warnings().is_empty());
/// ```
pub fn check_mixed_bundle(
bundle: &crate::source_project::SourceBundle,
limits: crate::source_project::ProjectLimits,
) -> Result<CheckedMixed<'_>, Vec<SourceDiagnostic>> {
let syntax = super::carried::parse_fresh(bundle, limits)?;
let resolved = resolve(syntax).map_err(|failure| failure.diagnostics)?;
Ok(CheckedMixed { resolved })
}
/// Read-only authority: every component originates in the consumed syntax.
pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> {
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
syntax: ParsedMixedSyntax<'s>,
project: Project,
warnings: Vec<SourceDiagnostic>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_limits: analysis::AdmissionLimits,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
static_usage: analysis::AdmissionUsage,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
expressions: Vec<lower::ExpressionSite>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
occurrences: Vec<lower::Occurrence>,
// Retained ownership/admission evidence; consumed by the private test-only edit chain.
#[cfg_attr(not(test), allow(dead_code))]
storage: Ledger,
}
impl ResolvedMixedProject<'_> {
@@ -51,10 +142,12 @@ impl ResolvedMixedProject<'_> {
/// Complete original bundle authority with a typed-only occurrence view.
/// Ordinary globals are catalogued, but their authored occurrences are not indexed.
/// This owner cannot be converted into the legacy typed-only edit authority.
#[cfg(test)]
pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> {
resolved: ResolvedMixedProject<'s>,
data: super::super::index::IndexData,
}
#[cfg(test)]
impl<'s> ResolvedMixedProject<'s> {
pub fn index(mut self) -> Result<IndexedMixedProject<'s>, Failure> {
let input = IndexInput {
@@ -72,6 +165,7 @@ impl<'s> ResolvedMixedProject<'s> {
})
}
}
#[cfg(test)]
impl IndexedMixedProject<'_> {
pub fn project(&self) -> &Project {
self.resolved.project()
@@ -108,12 +202,14 @@ impl IndexedMixedProject<'_> {
/// Constructor-only borrowed adapter. Its private file table prevents fabrication;
/// no API returns it and no alternative source/token table can be installed.
#[cfg(test)]
pub(in crate::language::analysis_source) struct IndexInput<'a> {
files: &'a [FileSyntax<'a>],
pub(in crate::language::analysis_source) project: &'a Project,
pub(in crate::language::analysis_source) expressions: &'a [lower::ExpressionSite],
pub(in crate::language::analysis_source) occurrences: &'a [lower::Occurrence],
}
#[cfg(test)]
impl IndexInput<'_> {
pub(in crate::language::analysis_source) fn declarations(
&self,
@@ -140,6 +236,7 @@ pub(in crate::language::analysis_source) struct Failure {
}
impl Failure {
/// Canonical attribution when justified; otherwise a safe explicit start fallback.
#[cfg(test)]
fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self {
failure.file = failure.file.filter(|&f| f < syntax.files.len());
if failure.file.is_none_or(|file| {