2037 lines
67 KiB
Rust
2037 lines
67 KiB
Rust
//! Current-text symbol queries, with no model or editor mutation.
|
|
//!
|
|
//! The caller supplies one immutable snapshot of current text, full lexer tokens,
|
|
//! and syntax-only declaration role spans. This module does not lex, parse
|
|
//! Arcadia declarations, lint, compile, allocate model IDs, discover includes,
|
|
//! or read an accepted Project.
|
|
//! Its matches are authored lexical uses, not typed semantic dependency edges.
|
|
|
|
use oa_core::Id;
|
|
use oa_core::language::{Span, TokenClass, TokenSpan};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
use std::ops::Range;
|
|
|
|
/// Fresh editor/workspace session identity plus a revision changed for EVERY
|
|
/// indexed input change, including membership, outline and validation evidence.
|
|
/// Reusing this key for different input violates the caller contract.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct SnapshotKey {
|
|
pub workspace: Id,
|
|
pub revision: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Membership {
|
|
Included,
|
|
Detached,
|
|
Unverified,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Evidence {
|
|
/// The caller verified these exact bytes in the current source scope.
|
|
Validated,
|
|
/// Current editor bytes; no successful semantic validation is claimed.
|
|
Draft,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SyntaxCoverage {
|
|
/// The tolerant walk considered the entire file, even if its text is invalid.
|
|
FullFile,
|
|
/// A stopped/budget-limited outline cannot masquerade as a complete result.
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Defensive input contract exercised by index tests."
|
|
)
|
|
)]
|
|
Incomplete,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PartialReason {
|
|
IncludesUnverified,
|
|
MissingFiles,
|
|
UnappliedWorkspace,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Scope {
|
|
CompleteProject,
|
|
Partial(PartialReason),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Domain {
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested included-only queries; editor presents all loaded candidates."
|
|
)
|
|
)]
|
|
Included,
|
|
Loaded,
|
|
}
|
|
|
|
impl Domain {
|
|
fn contains(self, membership: Membership) -> bool {
|
|
self == Self::Loaded || membership == Membership::Included
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct AuthoredIdentity {
|
|
pub id: Id,
|
|
/// Whole quoted JSON string token in the declaration header, not a UUID
|
|
/// mentioned in a body, history record, comment or display name.
|
|
pub literal_span: Span,
|
|
}
|
|
|
|
/// These header roles must come from the coordinated core outline seam. The
|
|
/// existing public DeclarationSpan alone lacks alias/kind/ID literal spans.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct DeclarationInput {
|
|
pub span: Span,
|
|
pub kind_span: Span,
|
|
pub alias_span: Span,
|
|
pub identity: Option<AuthoredIdentity>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct FileInput<'a> {
|
|
pub path: &'a str,
|
|
pub text: &'a str,
|
|
/// Exact lexer output for text, in order and covering the entire file.
|
|
pub tokens: &'a [TokenSpan],
|
|
/// Tolerant outline in alias-source order. Whole spans may nest.
|
|
pub declarations: &'a [DeclarationInput],
|
|
pub syntax: SyntaxCoverage,
|
|
pub membership: Membership,
|
|
pub evidence: Evidence,
|
|
}
|
|
|
|
/// Explicit experiment limits; production must select its own shared budgets.
|
|
/// Caller-owned text/tokens/outline must already have bounded extraction.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct Limits {
|
|
pub files: usize,
|
|
pub source_bytes: usize,
|
|
pub path_bytes: usize,
|
|
pub tokens: usize,
|
|
pub records: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Resource {
|
|
Files,
|
|
SourceBytes,
|
|
PathBytes,
|
|
Tokens,
|
|
Records,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum InputProblem {
|
|
NonCanonicalPath,
|
|
DuplicateOrCaseCollidingPath,
|
|
InconsistentCompleteScope,
|
|
IncompleteSyntax,
|
|
TokenCoverage,
|
|
TokenBounds,
|
|
DeclarationBounds,
|
|
DeclarationOrder,
|
|
HeaderRoleToken,
|
|
InvalidAlias,
|
|
IdentityLiteral,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum BuildError {
|
|
/// No partial Index is returned. This is not a zero-result query.
|
|
Limit {
|
|
resource: Resource,
|
|
limit: usize,
|
|
observed_at_least: usize,
|
|
},
|
|
Input {
|
|
file: usize,
|
|
record: Option<usize>,
|
|
problem: InputProblem,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Location {
|
|
pub file: usize,
|
|
pub span: Span,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct DefinitionHandle {
|
|
snapshot: SnapshotKey,
|
|
index: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ReferenceHandle {
|
|
snapshot: SnapshotKey,
|
|
index: usize,
|
|
}
|
|
|
|
/// Carries only an authored identity, never an identity generated by compilation.
|
|
/// It may be resolved across revisions of the same workspace. Resolution still
|
|
/// checks global loaded-file uniqueness every time.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct AuthoredAnchor {
|
|
workspace: Id,
|
|
id: Id,
|
|
}
|
|
|
|
impl AuthoredAnchor {
|
|
pub fn id(self) -> Id {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Identity {
|
|
Missing,
|
|
Unique(AuthoredAnchor),
|
|
Collision { count: usize },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested unique-reference and authored-anchor projections."
|
|
)
|
|
)]
|
|
pub enum Lookup {
|
|
Missing,
|
|
Unique(DefinitionHandle),
|
|
Ambiguous { count: usize },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Hit {
|
|
Definition(DefinitionHandle),
|
|
Reference(ReferenceHandle),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum QueryError {
|
|
StaleSnapshot,
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Authored-anchor projection rejects cross-workspace identity."
|
|
)
|
|
)]
|
|
WrongWorkspace,
|
|
InvalidHandle,
|
|
InvalidFile,
|
|
InvalidBytePosition,
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(dead_code, reason = "Tested included-only reference projection.")
|
|
)]
|
|
OutsideDomain,
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(dead_code, reason = "Tested unique-declaration reference projection.")
|
|
)]
|
|
AmbiguousAlias {
|
|
count: usize,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested primitive scope; editor exposes adapter include inspection."
|
|
)
|
|
)]
|
|
pub struct QueryScope {
|
|
pub declared: Scope,
|
|
pub domain: Domain,
|
|
/// True only for the complete included project, not loaded detached files.
|
|
pub project_complete: bool,
|
|
pub all_selected_files_validated: bool,
|
|
pub selected_files: usize,
|
|
pub detached_files: usize,
|
|
pub unverified_files: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Position {
|
|
pub line: usize,
|
|
/// Zero-based UTF-8 byte column. The editor adapter must convert explicitly
|
|
/// if it needs character/UTF-16 columns; CRLF bytes are never normalized.
|
|
pub byte_column: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Definition<'a> {
|
|
pub alias: &'a str,
|
|
pub kind: &'a str,
|
|
pub declaration: Location,
|
|
pub alias_location: Location,
|
|
pub authored_id: Option<Id>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Reference<'a> {
|
|
pub alias: &'a str,
|
|
pub at: Location,
|
|
pub alias_location: Location,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SourceFile<'a> {
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Retained source projection for independently tested index callers."
|
|
)
|
|
)]
|
|
pub path: &'a str,
|
|
pub text: &'a str,
|
|
pub membership: Membership,
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Included-only scope projection retains validation evidence."
|
|
)
|
|
)]
|
|
pub evidence: Evidence,
|
|
definitions: Range<usize>,
|
|
references: Range<usize>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Index<'a> {
|
|
snapshot: SnapshotKey,
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested domain scope projection; adapter owns the UI scope."
|
|
)
|
|
)]
|
|
scope: Scope,
|
|
files: Vec<SourceFile<'a>>,
|
|
definitions: Vec<Definition<'a>>,
|
|
references: Vec<Reference<'a>>,
|
|
aliases: BTreeMap<&'a str, Vec<usize>>,
|
|
uses: BTreeMap<&'a str, Vec<usize>>,
|
|
identities: BTreeMap<Id, Vec<usize>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct Counts {
|
|
definitions: usize,
|
|
references: usize,
|
|
}
|
|
|
|
fn charge(
|
|
total: &mut usize,
|
|
value: usize,
|
|
limit: usize,
|
|
resource: Resource,
|
|
) -> Result<(), BuildError> {
|
|
let next = total.checked_add(value).ok_or(BuildError::Limit {
|
|
resource,
|
|
limit,
|
|
observed_at_least: usize::MAX,
|
|
})?;
|
|
if next > limit {
|
|
return Err(BuildError::Limit {
|
|
resource,
|
|
limit,
|
|
observed_at_least: next,
|
|
});
|
|
}
|
|
*total = next;
|
|
Ok(())
|
|
}
|
|
|
|
fn input_error(file: usize, record: Option<usize>, problem: InputProblem) -> BuildError {
|
|
BuildError::Input {
|
|
file,
|
|
record,
|
|
problem,
|
|
}
|
|
}
|
|
|
|
fn valid_span(text: &str, span: Span) -> bool {
|
|
span.start < span.end
|
|
&& span.end <= text.len()
|
|
&& text.is_char_boundary(span.start)
|
|
&& text.is_char_boundary(span.end)
|
|
}
|
|
|
|
fn contains(outer: Span, inner: Span) -> bool {
|
|
outer.start <= inner.start && inner.end <= outer.end
|
|
}
|
|
|
|
fn token_at(tokens: &[TokenSpan], span: Span) -> Option<TokenSpan> {
|
|
let index = tokens
|
|
.binary_search_by_key(&span.start, |token| token.span.start)
|
|
.ok()?;
|
|
let token = tokens[index];
|
|
(token.span == span).then_some(token)
|
|
}
|
|
|
|
fn word(class: TokenClass) -> bool {
|
|
matches!(class, TokenClass::Identifier | TokenClass::Keyword)
|
|
}
|
|
|
|
/// Only a live punctuation @ followed by a legal alias outside trivia qualifies.
|
|
/// Trivia can separate @ and alias, but its bytes never become a clickable hit.
|
|
fn reference_spans<'a>(file: FileInput<'a>) -> impl Iterator<Item = (Span, Span)> + 'a {
|
|
file.tokens
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(move |(index, token)| {
|
|
if token.class != TokenClass::Punctuation
|
|
|| &file.text[token.span.start..token.span.end] != "@"
|
|
{
|
|
return None;
|
|
}
|
|
let next = file.tokens[index + 1..]
|
|
.iter()
|
|
.find(|next| !matches!(next.class, TokenClass::Whitespace | TokenClass::Comment))?;
|
|
let alias = &file.text[next.span.start..next.span.end];
|
|
(word(next.class) && oa_core::valid_alias(alias)).then_some((token.span, next.span))
|
|
})
|
|
}
|
|
|
|
/// Runs all aggregate limits and structural checks before allocating Index maps
|
|
/// or vectors. Path normalization/quoted-ID decoding may allocate bounded scratch
|
|
/// strings. Scratch storage is proportional to admitted input bytes and file
|
|
/// count, including Unicode expansion and collection overhead.
|
|
fn preflight(files: &[FileInput<'_>], scope: Scope, limits: Limits) -> Result<Counts, BuildError> {
|
|
let mut file_count = 0;
|
|
charge(&mut file_count, files.len(), limits.files, Resource::Files)?;
|
|
let mut bytes = 0;
|
|
let mut path_bytes = 0;
|
|
let mut tokens = 0;
|
|
let mut records = 0;
|
|
for file in files {
|
|
charge(
|
|
&mut bytes,
|
|
file.text.len(),
|
|
limits.source_bytes,
|
|
Resource::SourceBytes,
|
|
)?;
|
|
charge(
|
|
&mut path_bytes,
|
|
file.path.len(),
|
|
limits.path_bytes,
|
|
Resource::PathBytes,
|
|
)?;
|
|
charge(
|
|
&mut tokens,
|
|
file.tokens.len(),
|
|
limits.tokens,
|
|
Resource::Tokens,
|
|
)?;
|
|
charge(
|
|
&mut records,
|
|
file.declarations.len(),
|
|
limits.records,
|
|
Resource::Records,
|
|
)?;
|
|
}
|
|
let definitions = records;
|
|
if scope == Scope::CompleteProject
|
|
&& (!files
|
|
.iter()
|
|
.any(|file| file.membership == Membership::Included)
|
|
|| files
|
|
.iter()
|
|
.any(|file| file.membership == Membership::Unverified))
|
|
{
|
|
return Err(input_error(
|
|
0,
|
|
None,
|
|
InputProblem::InconsistentCompleteScope,
|
|
));
|
|
}
|
|
// Scratch storage is allocated only after aggregate file/path admission.
|
|
// Each admitted path is folded once, retaining core's Unicode policy.
|
|
let mut folded_paths = BTreeSet::new();
|
|
for (file_index, file) in files.iter().copied().enumerate() {
|
|
if file.syntax != SyntaxCoverage::FullFile {
|
|
return Err(input_error(
|
|
file_index,
|
|
None,
|
|
InputProblem::IncompleteSyntax,
|
|
));
|
|
}
|
|
if oa_core::source_project::resolve_include("", file.path).as_deref() != Ok(file.path) {
|
|
return Err(input_error(
|
|
file_index,
|
|
None,
|
|
InputProblem::NonCanonicalPath,
|
|
));
|
|
}
|
|
if !folded_paths.insert(file.path.to_lowercase()) {
|
|
return Err(input_error(
|
|
file_index,
|
|
None,
|
|
InputProblem::DuplicateOrCaseCollidingPath,
|
|
));
|
|
}
|
|
let mut end = 0;
|
|
for (token_index, token) in file.tokens.iter().enumerate() {
|
|
if !valid_span(file.text, token.span) {
|
|
return Err(input_error(
|
|
file_index,
|
|
Some(token_index),
|
|
InputProblem::TokenBounds,
|
|
));
|
|
}
|
|
if token.span.start != end {
|
|
return Err(input_error(
|
|
file_index,
|
|
Some(token_index),
|
|
InputProblem::TokenCoverage,
|
|
));
|
|
}
|
|
end = token.span.end;
|
|
}
|
|
if end != file.text.len() {
|
|
return Err(input_error(file_index, None, InputProblem::TokenCoverage));
|
|
}
|
|
let mut previous_alias_end = 0;
|
|
for (declaration_index, declaration) in file.declarations.iter().enumerate() {
|
|
let problem = |problem| input_error(file_index, Some(declaration_index), problem);
|
|
if !valid_span(file.text, declaration.span)
|
|
|| !valid_span(file.text, declaration.kind_span)
|
|
|| !valid_span(file.text, declaration.alias_span)
|
|
|| declaration.kind_span.start != declaration.span.start
|
|
|| !contains(declaration.span, declaration.kind_span)
|
|
|| !contains(declaration.span, declaration.alias_span)
|
|
|| declaration.alias_span.start < declaration.kind_span.end
|
|
{
|
|
return Err(problem(InputProblem::DeclarationBounds));
|
|
}
|
|
if declaration.alias_span.start < previous_alias_end {
|
|
return Err(problem(InputProblem::DeclarationOrder));
|
|
}
|
|
previous_alias_end = declaration.alias_span.end;
|
|
if !token_at(file.tokens, declaration.kind_span).is_some_and(|token| word(token.class))
|
|
|| !token_at(file.tokens, declaration.alias_span)
|
|
.is_some_and(|token| word(token.class))
|
|
{
|
|
return Err(problem(InputProblem::HeaderRoleToken));
|
|
}
|
|
let alias = &file.text[declaration.alias_span.start..declaration.alias_span.end];
|
|
if !oa_core::valid_alias(alias) {
|
|
return Err(problem(InputProblem::InvalidAlias));
|
|
}
|
|
if let Some(identity) = declaration.identity {
|
|
if !valid_span(file.text, identity.literal_span)
|
|
|| !contains(declaration.span, identity.literal_span)
|
|
|| identity.literal_span.start < declaration.alias_span.end
|
|
|| token_at(file.tokens, identity.literal_span).map(|token| token.class)
|
|
!= Some(TokenClass::String)
|
|
{
|
|
return Err(problem(InputProblem::IdentityLiteral));
|
|
}
|
|
let literal = &file.text[identity.literal_span.start..identity.literal_span.end];
|
|
let decoded = serde_json::from_str::<String>(literal)
|
|
.ok()
|
|
.filter(|value| oa_core::language::has_uuid_header_shape(value))
|
|
.and_then(|value| Id::parse_str(&value).ok());
|
|
if decoded != Some(identity.id) {
|
|
return Err(problem(InputProblem::IdentityLiteral));
|
|
}
|
|
}
|
|
}
|
|
// Token bounds and coverage are proven before reference_spans slices.
|
|
for _ in reference_spans(file) {
|
|
charge(&mut records, 1, limits.records, Resource::Records)?;
|
|
}
|
|
}
|
|
Ok(Counts {
|
|
definitions,
|
|
references: records - definitions,
|
|
})
|
|
}
|
|
|
|
impl<'a> Index<'a> {
|
|
pub fn build(
|
|
snapshot: SnapshotKey,
|
|
files: &[FileInput<'a>],
|
|
scope: Scope,
|
|
limits: Limits,
|
|
) -> Result<Self, BuildError> {
|
|
let counts = preflight(files, scope, limits)?;
|
|
let mut index = Self {
|
|
snapshot,
|
|
scope,
|
|
files: Vec::with_capacity(files.len()),
|
|
definitions: Vec::with_capacity(counts.definitions),
|
|
references: Vec::with_capacity(counts.references),
|
|
aliases: BTreeMap::new(),
|
|
uses: BTreeMap::new(),
|
|
identities: BTreeMap::new(),
|
|
};
|
|
for (file_index, file) in files.iter().copied().enumerate() {
|
|
let definition_start = index.definitions.len();
|
|
let reference_start = index.references.len();
|
|
for declaration in file.declarations {
|
|
let alias = &file.text[declaration.alias_span.start..declaration.alias_span.end];
|
|
let number = index.definitions.len();
|
|
index.aliases.entry(alias).or_default().push(number);
|
|
if let Some(identity) = declaration.identity {
|
|
index
|
|
.identities
|
|
.entry(identity.id)
|
|
.or_default()
|
|
.push(number);
|
|
}
|
|
index.definitions.push(Definition {
|
|
alias,
|
|
kind: &file.text[declaration.kind_span.start..declaration.kind_span.end],
|
|
declaration: Location {
|
|
file: file_index,
|
|
span: declaration.span,
|
|
},
|
|
alias_location: Location {
|
|
file: file_index,
|
|
span: declaration.alias_span,
|
|
},
|
|
authored_id: declaration.identity.map(|identity| identity.id),
|
|
});
|
|
}
|
|
for (at, alias_span) in reference_spans(file) {
|
|
let alias = &file.text[alias_span.start..alias_span.end];
|
|
index
|
|
.uses
|
|
.entry(alias)
|
|
.or_default()
|
|
.push(index.references.len());
|
|
index.references.push(Reference {
|
|
alias,
|
|
at: Location {
|
|
file: file_index,
|
|
span: at,
|
|
},
|
|
alias_location: Location {
|
|
file: file_index,
|
|
span: alias_span,
|
|
},
|
|
});
|
|
}
|
|
index.files.push(SourceFile {
|
|
path: file.path,
|
|
text: file.text,
|
|
membership: file.membership,
|
|
evidence: file.evidence,
|
|
definitions: definition_start..index.definitions.len(),
|
|
references: reference_start..index.references.len(),
|
|
});
|
|
}
|
|
Ok(index)
|
|
}
|
|
|
|
pub fn source(&self, file: usize) -> Result<&SourceFile<'a>, QueryError> {
|
|
self.files.get(file).ok_or(QueryError::InvalidFile)
|
|
}
|
|
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested included/loaded scope projection; editor uses adapter scope."
|
|
)
|
|
)]
|
|
pub fn scope(&self, domain: Domain) -> QueryScope {
|
|
let selected_files = self
|
|
.files
|
|
.iter()
|
|
.filter(|file| domain.contains(file.membership))
|
|
.count();
|
|
QueryScope {
|
|
declared: self.scope,
|
|
domain,
|
|
project_complete: self.scope == Scope::CompleteProject && domain == Domain::Included,
|
|
all_selected_files_validated: selected_files != 0
|
|
&& self
|
|
.files
|
|
.iter()
|
|
.filter(|file| domain.contains(file.membership))
|
|
.all(|file| file.evidence == Evidence::Validated),
|
|
selected_files,
|
|
detached_files: self
|
|
.files
|
|
.iter()
|
|
.filter(|file| file.membership == Membership::Detached)
|
|
.count(),
|
|
unverified_files: self
|
|
.files
|
|
.iter()
|
|
.filter(|file| file.membership == Membership::Unverified)
|
|
.count(),
|
|
}
|
|
}
|
|
|
|
fn definition_handle(&self, index: usize) -> DefinitionHandle {
|
|
DefinitionHandle {
|
|
snapshot: self.snapshot,
|
|
index,
|
|
}
|
|
}
|
|
|
|
fn reference_handle(&self, index: usize) -> ReferenceHandle {
|
|
ReferenceHandle {
|
|
snapshot: self.snapshot,
|
|
index,
|
|
}
|
|
}
|
|
|
|
pub fn definition(&self, handle: DefinitionHandle) -> Result<&Definition<'a>, QueryError> {
|
|
if handle.snapshot != self.snapshot {
|
|
return Err(QueryError::StaleSnapshot);
|
|
}
|
|
self.definitions
|
|
.get(handle.index)
|
|
.ok_or(QueryError::InvalidHandle)
|
|
}
|
|
|
|
pub fn reference(&self, handle: ReferenceHandle) -> Result<&Reference<'a>, QueryError> {
|
|
if handle.snapshot != self.snapshot {
|
|
return Err(QueryError::StaleSnapshot);
|
|
}
|
|
self.references
|
|
.get(handle.index)
|
|
.ok_or(QueryError::InvalidHandle)
|
|
}
|
|
|
|
pub fn definitions_for_alias<'i>(&'i self, alias: &str, domain: Domain) -> Definitions<'i, 'a> {
|
|
Definitions {
|
|
index: self,
|
|
indices: self
|
|
.aliases
|
|
.get(alias)
|
|
.map_or(&[][..], Vec::as_slice)
|
|
.iter(),
|
|
domain,
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested authored-identity lookup; editor uses aliases and collision checks."
|
|
)
|
|
)]
|
|
pub fn definitions_for_id(&self, id: Id, domain: Domain) -> Definitions<'_, 'a> {
|
|
Definitions {
|
|
index: self,
|
|
indices: self
|
|
.identities
|
|
.get(&id)
|
|
.map_or(&[][..], Vec::as_slice)
|
|
.iter(),
|
|
domain,
|
|
}
|
|
}
|
|
|
|
pub fn references_for_alias<'i>(&'i self, alias: &str, domain: Domain) -> References<'i, 'a> {
|
|
References {
|
|
index: self,
|
|
indices: self.uses.get(alias).map_or(&[][..], Vec::as_slice).iter(),
|
|
domain,
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested unique-reference projection; UI shows bounded candidate rows."
|
|
)
|
|
)]
|
|
pub fn resolve_reference(
|
|
&self,
|
|
handle: ReferenceHandle,
|
|
domain: Domain,
|
|
) -> Result<Lookup, QueryError> {
|
|
let reference = self.reference(handle)?;
|
|
if !domain.contains(self.files[reference.at.file].membership) {
|
|
return Err(QueryError::OutsideDomain);
|
|
}
|
|
let mut definitions = self.definitions_for_alias(reference.alias, domain);
|
|
let Some(first) = definitions.next() else {
|
|
return Ok(Lookup::Missing);
|
|
};
|
|
let count = 1 + definitions.count();
|
|
Ok(if count == 1 {
|
|
Lookup::Unique(first)
|
|
} else {
|
|
Lookup::Ambiguous { count }
|
|
})
|
|
}
|
|
|
|
pub fn identity(&self, handle: DefinitionHandle) -> Result<Identity, QueryError> {
|
|
let Some(id) = self.definition(handle)?.authored_id else {
|
|
return Ok(Identity::Missing);
|
|
};
|
|
let count = self.identities[&id].len();
|
|
Ok(if count == 1 {
|
|
Identity::Unique(AuthoredAnchor {
|
|
workspace: self.snapshot.workspace,
|
|
id,
|
|
})
|
|
} else {
|
|
Identity::Collision { count }
|
|
})
|
|
}
|
|
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested identity-safe anchor API; editor Back uses buffer generations."
|
|
)
|
|
)]
|
|
pub fn resolve_anchor(
|
|
&self,
|
|
anchor: AuthoredAnchor,
|
|
domain: Domain,
|
|
) -> Result<Lookup, QueryError> {
|
|
if anchor.workspace != self.snapshot.workspace {
|
|
return Err(QueryError::WrongWorkspace);
|
|
}
|
|
let Some(candidates) = self.identities.get(&anchor.id) else {
|
|
return Ok(Lookup::Missing);
|
|
};
|
|
// A detached duplicate must not silently confer identity uniqueness.
|
|
if candidates.len() != 1 {
|
|
return Ok(Lookup::Ambiguous {
|
|
count: candidates.len(),
|
|
});
|
|
}
|
|
let candidate = candidates[0];
|
|
Ok(
|
|
if domain.contains(self.files[self.definitions[candidate].declaration.file].membership)
|
|
{
|
|
Lookup::Unique(self.definition_handle(candidate))
|
|
} else {
|
|
Lookup::Missing
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Exact authored alias-use projection for a unique current declaration in
|
|
/// the requested domain. Duplicate aliases require candidate inspection;
|
|
/// this API never attributes all matching uses to the first declaration.
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested unique-target projection; UI labels lexical uses explicitly."
|
|
)
|
|
)]
|
|
pub fn references_to(
|
|
&self,
|
|
handle: DefinitionHandle,
|
|
domain: Domain,
|
|
) -> Result<References<'_, 'a>, QueryError> {
|
|
let definition = self.definition(handle)?;
|
|
if !domain.contains(self.files[definition.declaration.file].membership) {
|
|
return Err(QueryError::OutsideDomain);
|
|
}
|
|
let count = self.definitions_for_alias(definition.alias, domain).count();
|
|
if count != 1 {
|
|
return Err(QueryError::AmbiguousAlias { count });
|
|
}
|
|
Ok(self.references_for_alias(definition.alias, domain))
|
|
}
|
|
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested exact single-location projection; adapter batches projections."
|
|
)
|
|
)]
|
|
pub fn position(&self, file: usize, offset: usize) -> Result<Position, QueryError> {
|
|
let source = self.source(file)?;
|
|
if offset > source.text.len() || !source.text.is_char_boundary(offset) {
|
|
return Err(QueryError::InvalidBytePosition);
|
|
}
|
|
let prefix = &source.text[..offset];
|
|
Ok(Position {
|
|
line: prefix.bytes().filter(|byte| *byte == b'\n').count(),
|
|
byte_column: prefix
|
|
.rsplit_once('\n')
|
|
.map_or(prefix.len(), |(_, tail)| tail.len()),
|
|
})
|
|
}
|
|
|
|
pub fn hit(&self, file: usize, offset: usize) -> Result<Option<Hit>, QueryError> {
|
|
let source = self.source(file)?;
|
|
if offset > source.text.len() || !source.text.is_char_boundary(offset) {
|
|
return Err(QueryError::InvalidBytePosition);
|
|
}
|
|
let definitions = &self.definitions[source.definitions.clone()];
|
|
let before = definitions
|
|
.partition_point(|definition| definition.alias_location.span.start <= offset);
|
|
if before != 0 && offset < definitions[before - 1].alias_location.span.end {
|
|
return Ok(Some(Hit::Definition(
|
|
self.definition_handle(source.definitions.start + before - 1),
|
|
)));
|
|
}
|
|
let references = &self.references[source.references.clone()];
|
|
let before = references.partition_point(|reference| reference.at.span.start <= offset);
|
|
if before != 0 {
|
|
let reference = &references[before - 1];
|
|
if offset < reference.at.span.end
|
|
|| (reference.alias_location.span.start <= offset
|
|
&& offset < reference.alias_location.span.end)
|
|
{
|
|
return Ok(Some(Hit::Reference(
|
|
self.reference_handle(source.references.start + before - 1),
|
|
)));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub struct Definitions<'i, 'a> {
|
|
index: &'i Index<'a>,
|
|
indices: std::slice::Iter<'i, usize>,
|
|
domain: Domain,
|
|
}
|
|
|
|
impl Iterator for Definitions<'_, '_> {
|
|
type Item = DefinitionHandle;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
for &number in self.indices.by_ref() {
|
|
let definition = &self.index.definitions[number];
|
|
if self
|
|
.domain
|
|
.contains(self.index.files[definition.declaration.file].membership)
|
|
{
|
|
return Some(self.index.definition_handle(number));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
pub struct References<'i, 'a> {
|
|
index: &'i Index<'a>,
|
|
indices: std::slice::Iter<'i, usize>,
|
|
domain: Domain,
|
|
}
|
|
|
|
impl References<'_, '_> {
|
|
#[cfg_attr(
|
|
not(test),
|
|
expect(
|
|
dead_code,
|
|
reason = "Tested primitive scope; editor displays adapter results scope."
|
|
)
|
|
)]
|
|
pub fn scope(&self) -> QueryScope {
|
|
self.index.scope(self.domain)
|
|
}
|
|
}
|
|
|
|
impl Iterator for References<'_, '_> {
|
|
type Item = ReferenceHandle;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
for &number in self.indices.by_ref() {
|
|
let reference = &self.index.references[number];
|
|
if self
|
|
.domain
|
|
.contains(self.index.files[reference.at.file].membership)
|
|
{
|
|
return Some(self.index.reference_handle(number));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const A: Id = Id::from_u128(1);
|
|
const B: Id = Id::from_u128(2);
|
|
const C: Id = Id::from_u128(3);
|
|
|
|
struct Fixture {
|
|
path: String,
|
|
text: String,
|
|
tokens: Vec<TokenSpan>,
|
|
declarations: Vec<DeclarationInput>,
|
|
}
|
|
|
|
impl Fixture {
|
|
/// Test-only literal role proof. Production never searches declaration
|
|
/// headers: its caller supplies the coordinated syntax outline roles.
|
|
fn new(path: &str, text: String, roles: &[(&str, &str, Option<Id>)]) -> Self {
|
|
let tokens = oa_core::language::tokenize(&text);
|
|
let declarations = roles
|
|
.iter()
|
|
.map(|&(kind, alias, id)| {
|
|
let header = format!("{kind} {alias} ");
|
|
let start = text.find(&header).expect("authored fixture header");
|
|
let end = text[start..]
|
|
.find('}')
|
|
.map_or(text.len(), |end| start + end + 1);
|
|
let alias_start = start + kind.len() + 1;
|
|
DeclarationInput {
|
|
span: Span { start, end },
|
|
kind_span: Span {
|
|
start,
|
|
end: start + kind.len(),
|
|
},
|
|
alias_span: Span {
|
|
start: alias_start,
|
|
end: alias_start + alias.len(),
|
|
},
|
|
identity: id.map(|id| {
|
|
let literal = format!("\"{id}\"");
|
|
let literal_start = start
|
|
+ text[start..end]
|
|
.find(&literal)
|
|
.expect("authored fixture UUID");
|
|
AuthoredIdentity {
|
|
id,
|
|
literal_span: Span {
|
|
start: literal_start,
|
|
end: literal_start + literal.len(),
|
|
},
|
|
}
|
|
}),
|
|
}
|
|
})
|
|
.collect();
|
|
Self {
|
|
path: path.into(),
|
|
text,
|
|
tokens,
|
|
declarations,
|
|
}
|
|
}
|
|
|
|
fn input(&self, membership: Membership, evidence: Evidence) -> FileInput<'_> {
|
|
FileInput {
|
|
path: &self.path,
|
|
text: &self.text,
|
|
tokens: &self.tokens,
|
|
declarations: &self.declarations,
|
|
syntax: SyntaxCoverage::FullFile,
|
|
membership,
|
|
evidence,
|
|
}
|
|
}
|
|
|
|
fn included(&self) -> FileInput<'_> {
|
|
self.input(Membership::Included, Evidence::Draft)
|
|
}
|
|
}
|
|
|
|
fn snapshot(revision: u64) -> SnapshotKey {
|
|
SnapshotKey {
|
|
workspace: Id::from_u128(1000),
|
|
revision,
|
|
}
|
|
}
|
|
|
|
fn limits() -> Limits {
|
|
Limits {
|
|
files: 16,
|
|
source_bytes: 1024 * 1024,
|
|
path_bytes: 8192,
|
|
tokens: 200_000,
|
|
records: 100_000,
|
|
}
|
|
}
|
|
|
|
fn index<'a>(files: &[FileInput<'a>]) -> Index<'a> {
|
|
Index::build(snapshot(1), files, Scope::CompleteProject, limits()).unwrap()
|
|
}
|
|
|
|
fn definition(index: &Index<'_>, alias: &str) -> DefinitionHandle {
|
|
let mut found = index.definitions_for_alias(alias, Domain::Loaded);
|
|
let result = found.next().expect("fixture definition");
|
|
assert!(found.next().is_none(), "fixture expects unique alias");
|
|
result
|
|
}
|
|
|
|
fn anchor(index: &Index<'_>, handle: DefinitionHandle) -> AuthoredAnchor {
|
|
match index.identity(handle).unwrap() {
|
|
Identity::Unique(anchor) => anchor,
|
|
identity => panic!("fixture expects unique authored identity: {identity:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_live_uses_are_distinct_and_strings_comments_history_are_excluded() {
|
|
let fixture = Fixture::new(
|
|
"project.arcadia",
|
|
format!(
|
|
"oa_actor tank \"Same display\" id \"{A}\" {{}}\n\
|
|
oa_actor tank_extra \"Same display\" id \"{B}\" {{}}\n\
|
|
oa_interaction flow \"Flow\" id \"{C}\" {{ source @tank; target @tank; other @tank_extra; \
|
|
description \"@tank @tank_extra\"; historical uuid \"{A}\"; }}\n\
|
|
// @tank @tank_extra and UUID {A}\n"
|
|
),
|
|
&[
|
|
("oa_actor", "tank", Some(A)),
|
|
("oa_actor", "tank_extra", Some(B)),
|
|
("oa_interaction", "flow", Some(C)),
|
|
],
|
|
);
|
|
let original = (
|
|
fixture.text.clone(),
|
|
fixture.tokens.clone(),
|
|
fixture.declarations.clone(),
|
|
);
|
|
let index = index(&[fixture.included()]);
|
|
let tank = definition(&index, "tank");
|
|
let uses: Vec<_> = index
|
|
.references_to(tank, Domain::Included)
|
|
.unwrap()
|
|
.collect();
|
|
assert_eq!(uses.len(), 2);
|
|
assert_ne!(uses[0], uses[1]);
|
|
assert_eq!(
|
|
index
|
|
.references_for_alias("tank_extra", Domain::Loaded)
|
|
.count(),
|
|
1
|
|
);
|
|
for usage in uses {
|
|
let record = index.reference(usage).unwrap();
|
|
assert_eq!(&fixture.text[record.at.span.start..record.at.span.end], "@");
|
|
assert_eq!(
|
|
&fixture.text[record.alias_location.span.start..record.alias_location.span.end],
|
|
"tank"
|
|
);
|
|
assert_eq!(
|
|
index.resolve_reference(usage, Domain::Included),
|
|
Ok(Lookup::Unique(tank))
|
|
);
|
|
assert_eq!(
|
|
index.hit(0, record.alias_location.span.start),
|
|
Ok(Some(Hit::Reference(usage)))
|
|
);
|
|
}
|
|
for ignored in [
|
|
fixture.text.find("Same display").unwrap(),
|
|
fixture.text.find("// @tank").unwrap() + 3,
|
|
fixture.text.find("\"@tank @tank_extra\"").unwrap() + 1,
|
|
fixture.text.find("historical uuid").unwrap() + "historical uuid \"".len(),
|
|
] {
|
|
assert_eq!(index.hit(0, ignored), Ok(None));
|
|
}
|
|
assert_eq!(index.hit(0, fixture.text.len()), Ok(None));
|
|
assert_eq!(
|
|
original,
|
|
(
|
|
fixture.text.clone(),
|
|
fixture.tokens.clone(),
|
|
fixture.declarations.clone()
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn comment_separated_reference_keeps_crlf_unicode_and_disjoint_hit_spans() {
|
|
let fixture = Fixture::new(
|
|
"parts/actors.arcadia",
|
|
format!(
|
|
"oa_actor a \"救助 🚒\" id \"{A}\" {{}}\r\nsource @ // 注释 @a\r\n a; target @a;\r\n"
|
|
),
|
|
&[("oa_actor", "a", Some(A))],
|
|
);
|
|
let index = index(&[fixture.included()]);
|
|
let uses: Vec<_> = index.references_for_alias("a", Domain::Loaded).collect();
|
|
assert_eq!(uses.len(), 2);
|
|
let first = index.reference(uses[0]).unwrap();
|
|
assert_eq!(
|
|
index.position(0, first.at.span.start),
|
|
Ok(Position {
|
|
line: 1,
|
|
byte_column: 7
|
|
})
|
|
);
|
|
assert_eq!(
|
|
index.position(0, first.alias_location.span.start),
|
|
Ok(Position {
|
|
line: 2,
|
|
byte_column: 2
|
|
})
|
|
);
|
|
assert_eq!(
|
|
index.hit(0, first.at.span.start),
|
|
Ok(Some(Hit::Reference(uses[0])))
|
|
);
|
|
assert_eq!(
|
|
index.hit(0, first.alias_location.span.start),
|
|
Ok(Some(Hit::Reference(uses[0])))
|
|
);
|
|
for byte in [
|
|
first.at.span.end,
|
|
fixture.text.find("注释").unwrap(),
|
|
fixture.text.find("// 注释 @a").unwrap() + "// 注释 ".len(),
|
|
first.alias_location.span.end,
|
|
] {
|
|
assert_eq!(index.hit(0, byte), Ok(None));
|
|
}
|
|
let unicode_inside = fixture.text.find('救').unwrap() + 1;
|
|
assert_eq!(
|
|
index.hit(0, unicode_inside),
|
|
Err(QueryError::InvalidBytePosition)
|
|
);
|
|
assert_eq!(
|
|
index.position(0, unicode_inside),
|
|
Err(QueryError::InvalidBytePosition)
|
|
);
|
|
assert_eq!(
|
|
index.hit(0, fixture.text.len() + 1),
|
|
Err(QueryError::InvalidBytePosition)
|
|
);
|
|
assert_eq!(index.hit(1, 0), Err(QueryError::InvalidFile));
|
|
assert!(index.source(0).unwrap().text.contains("\r\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn legal_keyword_classified_alias_is_not_discarded() {
|
|
let fixture = Fixture::new(
|
|
"project.arcadia",
|
|
format!(
|
|
"oa_actor oa_activity \"Participant\" id \"{A}\" {{}}\nsource @oa_activity; source @parent;"
|
|
),
|
|
&[("oa_actor", "oa_activity", Some(A))],
|
|
);
|
|
assert!(oa_core::valid_alias("oa_activity"));
|
|
assert!(!oa_core::valid_alias("parent"));
|
|
assert_eq!(
|
|
token_at(&fixture.tokens, fixture.declarations[0].alias_span)
|
|
.unwrap()
|
|
.class,
|
|
TokenClass::Keyword
|
|
);
|
|
let index = index(&[fixture.included()]);
|
|
let found = definition(&index, "oa_activity");
|
|
let usage = index
|
|
.references_for_alias("oa_activity", Domain::Included)
|
|
.next()
|
|
.unwrap();
|
|
assert_eq!(
|
|
index.resolve_reference(usage, Domain::Included),
|
|
Ok(Lookup::Unique(found))
|
|
);
|
|
assert_eq!(
|
|
index.references_for_alias("parent", Domain::Loaded).count(),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
index.hit(0, fixture.declarations[0].alias_span.start),
|
|
Ok(Some(Hit::Definition(found)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_aliases_are_candidates_without_false_reference_attribution() {
|
|
let first = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor a \"Same display\" id \"{A}\" {{}}\nsource @a;"),
|
|
&[("oa_actor", "a", Some(A))],
|
|
);
|
|
let second = Fixture::new(
|
|
"parts/other.arcadia",
|
|
format!("oa_actor a \"Same display\" id \"{B}\" {{}}\nsource @a;"),
|
|
&[("oa_actor", "a", Some(B))],
|
|
);
|
|
let index = index(&[first.included(), second.included()]);
|
|
let candidates: Vec<_> = index.definitions_for_alias("a", Domain::Included).collect();
|
|
assert_eq!(candidates.len(), 2);
|
|
assert_eq!(index.definition(candidates[0]).unwrap().declaration.file, 0);
|
|
assert_eq!(index.definition(candidates[1]).unwrap().declaration.file, 1);
|
|
assert_ne!(anchor(&index, candidates[0]), anchor(&index, candidates[1]));
|
|
assert!(matches!(
|
|
index.references_to(candidates[0], Domain::Included),
|
|
Err(QueryError::AmbiguousAlias { count: 2 })
|
|
));
|
|
let uses: Vec<_> = index.references_for_alias("a", Domain::Included).collect();
|
|
assert_eq!(uses.len(), 2);
|
|
for usage in uses {
|
|
assert_eq!(
|
|
index.resolve_reference(usage, Domain::Included),
|
|
Ok(Lookup::Ambiguous { count: 2 })
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn detached_uuid_clone_disables_anchor_even_for_included_query() {
|
|
let first = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor a \"First\" id \"{A}\" {{}}"),
|
|
&[("oa_actor", "a", Some(A))],
|
|
);
|
|
let original = index(&[first.included()]);
|
|
let old_anchor = anchor(&original, definition(&original, "a"));
|
|
let detached = Fixture::new(
|
|
"retained.arcadia",
|
|
format!("oa_actor old \"Retained\" id \"{A}\" {{}}"),
|
|
&[("oa_actor", "old", Some(A))],
|
|
);
|
|
let current = Index::build(
|
|
snapshot(2),
|
|
&[
|
|
first.included(),
|
|
detached.input(Membership::Detached, Evidence::Draft),
|
|
],
|
|
Scope::CompleteProject,
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
current.identity(definition(¤t, "a")),
|
|
Ok(Identity::Collision { count: 2 })
|
|
);
|
|
assert_eq!(
|
|
current.identity(definition(¤t, "old")),
|
|
Ok(Identity::Collision { count: 2 })
|
|
);
|
|
assert_eq!(
|
|
current.resolve_anchor(old_anchor, Domain::Included),
|
|
Ok(Lookup::Ambiguous { count: 2 })
|
|
);
|
|
assert_eq!(
|
|
current.resolve_anchor(old_anchor, Domain::Loaded),
|
|
Ok(Lookup::Ambiguous { count: 2 })
|
|
);
|
|
assert_eq!(
|
|
current
|
|
.definitions_for_alias("old", Domain::Included)
|
|
.count(),
|
|
0
|
|
);
|
|
assert_eq!(current.definitions_for_id(A, Domain::Included).count(), 1);
|
|
assert_eq!(current.definitions_for_id(A, Domain::Loaded).count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn reused_alias_cannot_steal_old_identity_and_old_locations_are_stale() {
|
|
let first = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor a \"First\" id \"{A}\" {{}}\nsource @a;"),
|
|
&[("oa_actor", "a", Some(A))],
|
|
);
|
|
let original = index(&[first.included()]);
|
|
let old_definition = definition(&original, "a");
|
|
let old_reference = original
|
|
.references_for_alias("a", Domain::Included)
|
|
.next()
|
|
.unwrap();
|
|
let old_anchor = anchor(&original, old_definition);
|
|
let edited = Fixture::new(
|
|
"project.arcadia",
|
|
format!(
|
|
"oa_actor b \"First renamed\" id \"{A}\" {{}}\noa_actor a \"New actor\" id \"{B}\" {{}}\nsource @a; target @b;"
|
|
),
|
|
&[("oa_actor", "b", Some(A)), ("oa_actor", "a", Some(B))],
|
|
);
|
|
let current = Index::build(
|
|
snapshot(2),
|
|
&[edited.included()],
|
|
Scope::CompleteProject,
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
current
|
|
.definition(old_definition)
|
|
.map(|definition| definition.alias),
|
|
Err(QueryError::StaleSnapshot)
|
|
);
|
|
assert_eq!(
|
|
current
|
|
.reference(old_reference)
|
|
.map(|reference| reference.alias),
|
|
Err(QueryError::StaleSnapshot)
|
|
);
|
|
assert_eq!(
|
|
current.resolve_anchor(old_anchor, Domain::Included),
|
|
Ok(Lookup::Unique(definition(¤t, "b")))
|
|
);
|
|
let new_use = current
|
|
.references_for_alias("a", Domain::Included)
|
|
.next()
|
|
.unwrap();
|
|
assert_eq!(
|
|
current.resolve_reference(new_use, Domain::Included),
|
|
Ok(Lookup::Unique(definition(¤t, "a")))
|
|
);
|
|
assert_eq!(anchor(¤t, definition(¤t, "a")).id(), B);
|
|
let other_workspace = Index::build(
|
|
SnapshotKey {
|
|
workspace: Id::from_u128(2000),
|
|
revision: 2,
|
|
},
|
|
&[edited.included()],
|
|
Scope::CompleteProject,
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
other_workspace.resolve_anchor(old_anchor, Domain::Loaded),
|
|
Err(QueryError::WrongWorkspace)
|
|
);
|
|
assert_eq!(
|
|
other_workspace
|
|
.definition(definition(¤t, "b"))
|
|
.map(|definition| definition.alias),
|
|
Err(QueryError::StaleSnapshot)
|
|
);
|
|
let deleted = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor a \"New actor\" id \"{B}\" {{}}"),
|
|
&[("oa_actor", "a", Some(B))],
|
|
);
|
|
let after_delete = Index::build(
|
|
snapshot(3),
|
|
&[deleted.included()],
|
|
Scope::CompleteProject,
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
after_delete.resolve_anchor(old_anchor, Domain::Loaded),
|
|
Ok(Lookup::Missing)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_ids_produce_only_current_snapshot_locations_without_stabilization() {
|
|
let fixture = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor a \"No UUID\" {{ historical uuid \"{A}\"; }}\r\nsource @a;"),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
let original = fixture.text.clone();
|
|
let index = index(&[fixture.included()]);
|
|
let declaration = definition(&index, "a");
|
|
assert_eq!(index.identity(declaration), Ok(Identity::Missing));
|
|
assert_eq!(index.definition(declaration).unwrap().authored_id, None);
|
|
let usage = index
|
|
.references_to(declaration, Domain::Included)
|
|
.unwrap()
|
|
.next()
|
|
.unwrap();
|
|
assert_eq!(
|
|
index.resolve_reference(usage, Domain::Included),
|
|
Ok(Lookup::Unique(declaration))
|
|
);
|
|
assert_eq!(fixture.text, original);
|
|
assert_eq!(
|
|
index.source(0).unwrap().text.as_bytes(),
|
|
original.as_bytes()
|
|
);
|
|
assert!(!index.source(0).unwrap().text.contains(" id "));
|
|
assert_eq!(
|
|
index.hit(0, fixture.text.find(&A.to_string()).unwrap()),
|
|
Ok(None)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scope_provenance_and_detached_alias_collision_are_explicit() {
|
|
let included = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor a \"Included\" id \"{A}\" {{}}\nsource @a;"),
|
|
&[("oa_actor", "a", Some(A))],
|
|
);
|
|
let detached = Fixture::new(
|
|
"old.arcadia",
|
|
format!("oa_actor a \"Detached\" id \"{B}\" {{}}\nsource @a;"),
|
|
&[("oa_actor", "a", Some(B))],
|
|
);
|
|
let unknown = Fixture::new("new.arcadia", "source @a;".into(), &[]);
|
|
let index = Index::build(
|
|
snapshot(1),
|
|
&[
|
|
included.input(Membership::Included, Evidence::Validated),
|
|
detached.input(Membership::Detached, Evidence::Draft),
|
|
unknown.input(Membership::Unverified, Evidence::Draft),
|
|
],
|
|
Scope::Partial(PartialReason::IncludesUnverified),
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(index.references_for_alias("a", Domain::Included).count(), 1);
|
|
assert_eq!(index.references_for_alias("a", Domain::Loaded).count(), 3);
|
|
let scope = index.references_for_alias("a", Domain::Included).scope();
|
|
assert_eq!(
|
|
scope.declared,
|
|
Scope::Partial(PartialReason::IncludesUnverified)
|
|
);
|
|
assert!(!scope.project_complete);
|
|
assert!(scope.all_selected_files_validated);
|
|
assert_eq!(
|
|
(
|
|
scope.selected_files,
|
|
scope.detached_files,
|
|
scope.unverified_files
|
|
),
|
|
(1, 1, 1)
|
|
);
|
|
assert!(!index.scope(Domain::Loaded).all_selected_files_validated);
|
|
let uses: Vec<_> = index.references_for_alias("a", Domain::Loaded).collect();
|
|
assert!(matches!(
|
|
index.resolve_reference(uses[0], Domain::Included),
|
|
Ok(Lookup::Unique(_))
|
|
));
|
|
assert_eq!(
|
|
index.resolve_reference(uses[0], Domain::Loaded),
|
|
Ok(Lookup::Ambiguous { count: 2 })
|
|
);
|
|
assert_eq!(
|
|
index.resolve_reference(uses[1], Domain::Included),
|
|
Err(QueryError::OutsideDomain)
|
|
);
|
|
assert_eq!(
|
|
index.resolve_reference(uses[2], Domain::Included),
|
|
Err(QueryError::OutsideDomain)
|
|
);
|
|
assert_eq!(index.source(2).unwrap().membership, Membership::Unverified);
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[
|
|
included.included(),
|
|
unknown.input(Membership::Unverified, Evidence::Draft)
|
|
],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::InconsistentCompleteScope,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn incomplete_draft_is_lexical_and_error_token_does_not_leak_references() {
|
|
let fixture = Fixture::new(
|
|
"project.arcadia",
|
|
"oa_actor a \"Draft\" {}\nsource @a; ? \"unfinished @a".into(),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
assert!(
|
|
fixture
|
|
.tokens
|
|
.iter()
|
|
.any(|token| token.class == TokenClass::Error)
|
|
);
|
|
let index = Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::Partial(PartialReason::UnappliedWorkspace),
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
let matches = index
|
|
.references_to(definition(&index, "a"), Domain::Included)
|
|
.unwrap();
|
|
assert!(!matches.scope().project_complete);
|
|
assert!(!matches.scope().all_selected_files_validated);
|
|
assert_eq!(matches.count(), 1);
|
|
assert_eq!(index.hit(0, fixture.text.rfind("@a").unwrap()), Ok(None));
|
|
}
|
|
|
|
#[test]
|
|
fn header_metadata_must_match_current_token_boundaries_and_authored_id() {
|
|
let mut fixture = Fixture::new(
|
|
"project.arcadia",
|
|
format!("oa_actor alpha \"救助\" id \"{A}\" {{}}"),
|
|
&[("oa_actor", "alpha", Some(A))],
|
|
);
|
|
let original = fixture.declarations[0];
|
|
fixture.declarations[0].alias_span.start += 1;
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::HeaderRoleToken,
|
|
..
|
|
})
|
|
));
|
|
fixture.declarations[0] = original;
|
|
fixture.declarations[0].identity.as_mut().unwrap().id = B;
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::IdentityLiteral,
|
|
..
|
|
})
|
|
));
|
|
fixture.declarations[0] = original;
|
|
fixture.declarations[0].alias_span = Span {
|
|
start: fixture.text.find('救').unwrap() + 1,
|
|
end: fixture.text.find('救').unwrap() + 3,
|
|
};
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DeclarationBounds,
|
|
..
|
|
})
|
|
));
|
|
fixture.declarations[0] = original;
|
|
fixture.declarations[0].span.end = fixture.text.len() + 1;
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DeclarationBounds,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_or_gapped_tokens_refuse_even_when_project_scope_is_partial() {
|
|
let mut fixture = Fixture::new(
|
|
"project.arcadia",
|
|
"oa_actor a \"Text\" {}\nsource @a;".into(),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
let final_token = fixture.tokens.pop().unwrap();
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::Partial(PartialReason::MissingFiles),
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::TokenCoverage,
|
|
..
|
|
})
|
|
));
|
|
fixture.tokens.push(final_token);
|
|
fixture.tokens.remove(1);
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::TokenCoverage,
|
|
..
|
|
})
|
|
));
|
|
let mut unicode = Fixture::new("project.arcadia", "// 救助\n".into(), &[]);
|
|
unicode.tokens[0].span.end = unicode.text.find('救').unwrap() + 1;
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[unicode.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::TokenBounds,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn stopped_outline_is_not_a_successful_empty_query() {
|
|
let fixture = Fixture::new(
|
|
"project.arcadia",
|
|
"oa_actor a \"Text\" {}\nsource @a;".into(),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
let mut stopped = fixture.included();
|
|
stopped.declarations = &[];
|
|
stopped.syntax = SyntaxCoverage::Incomplete;
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[stopped],
|
|
Scope::Partial(PartialReason::MissingFiles),
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::IncompleteSyntax,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_declaration_extents_do_not_turn_body_text_into_definition_hits() {
|
|
let mut fixture = Fixture::new(
|
|
"project.arcadia",
|
|
"oa_entity outer \"Outer\" {\n oa_actor inner \"Inner\" {}\n}\nsource @inner;".into(),
|
|
&[("oa_entity", "outer", None), ("oa_actor", "inner", None)],
|
|
);
|
|
// Supply the actual outer whole span; nesting does not change alias order.
|
|
fixture.declarations[0].span.end = fixture.text.rfind('}').unwrap() + 1;
|
|
let index = index(&[fixture.included()]);
|
|
let inner = definition(&index, "inner");
|
|
assert_eq!(
|
|
index.hit(0, fixture.declarations[1].alias_span.start),
|
|
Ok(Some(Hit::Definition(inner)))
|
|
);
|
|
assert_eq!(
|
|
index.hit(0, fixture.text.find("\"Inner\"").unwrap() + 1),
|
|
Ok(None)
|
|
);
|
|
let usage = index
|
|
.references_for_alias("inner", Domain::Included)
|
|
.next()
|
|
.unwrap();
|
|
assert_eq!(
|
|
index.resolve_reference(usage, Domain::Included),
|
|
Ok(Lookup::Unique(inner))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn path_identity_rejects_normalization_and_case_collisions() {
|
|
let mut first = Fixture::new("project.arcadia", "source @a;".into(), &[]);
|
|
let second = Fixture::new("PROJECT.arcadia", "source @b;".into(), &[]);
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[first.included(), second.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DuplicateOrCaseCollidingPath,
|
|
..
|
|
})
|
|
));
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[first.included(), first.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DuplicateOrCaseCollidingPath,
|
|
..
|
|
})
|
|
));
|
|
for invalid in [
|
|
"./project.arcadia",
|
|
"parts/../project.arcadia",
|
|
"../project.arcadia",
|
|
"C:/project.arcadia",
|
|
"parts\\project.arcadia",
|
|
] {
|
|
first.path = invalid.into();
|
|
assert!(
|
|
matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[first.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::NonCanonicalPath,
|
|
..
|
|
})
|
|
),
|
|
"{invalid}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_path_collisions_match_core_lowercase_policy() {
|
|
for (first_path, second_path) in [
|
|
("Ä.arcadia", "ä.arcadia"),
|
|
("ΟΣ/model.arcadia", "ος/model.arcadia"),
|
|
] {
|
|
let first = Fixture::new(first_path, "source @a;".into(), &[]);
|
|
let second = Fixture::new(second_path, "source @b;".into(), &[]);
|
|
assert_eq!(first_path.to_lowercase(), second_path.to_lowercase());
|
|
assert!(!first_path.eq_ignore_ascii_case(second_path));
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[first.included(), second.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DuplicateOrCaseCollidingPath,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn noncanonical_header_identity_cannot_enter_through_query_metadata() {
|
|
for raw in [
|
|
"00000000000000000000000000000001",
|
|
"{00000000-0000-0000-0000-000000000001}",
|
|
"urn:uuid:00000000-0000-0000-0000-000000000001",
|
|
] {
|
|
let literal = format!("\"{raw}\"");
|
|
let source = format!("oa_actor a \"A\" id {literal} {{}}\nsource @a;");
|
|
let mut fixture = Fixture::new(
|
|
"project.arcadia",
|
|
source.clone(),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
let start = source.find(&literal).unwrap();
|
|
// This fixture has one explicit empty body. A brace inside the
|
|
// quoted UUID must not become the test's declaration end.
|
|
let body_start = source.find("{}\n").unwrap();
|
|
fixture.declarations[0].span.end = body_start + 2;
|
|
assert!(start + literal.len() <= body_start);
|
|
// An adapter passes None from the corrected outline; lexical
|
|
// definition/reference navigation survives without an ID anchor.
|
|
{
|
|
let admitted = index(&[fixture.included()]);
|
|
let d = definition(&admitted, "a");
|
|
assert_eq!(admitted.identity(d), Ok(Identity::Missing));
|
|
assert_eq!(
|
|
admitted.references_to(d, Domain::Included).unwrap().count(),
|
|
1
|
|
);
|
|
assert_eq!(admitted.source(0).unwrap().text, source);
|
|
}
|
|
// A forged/stale caller cannot put the UUID crate's permissive
|
|
// interpretation back into an authored anchor via the raw API.
|
|
fixture.declarations[0].identity = Some(AuthoredIdentity {
|
|
id: A,
|
|
literal_span: Span {
|
|
start,
|
|
end: start + literal.len(),
|
|
},
|
|
});
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::IdentityLiteral,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn query_header_admission_preserves_uppercase_and_json_escape_bytes() {
|
|
let expected = Id::from_u128(0xabcdef);
|
|
let uppercase = expected.to_string().to_uppercase();
|
|
for raw in [uppercase.clone(), format!("\\u0030{}", &uppercase[1..])] {
|
|
let literal = format!("\"{raw}\"");
|
|
let source = format!("oa_actor a \"A\" id {literal} {{}}\nsource @a;");
|
|
let mut fixture = Fixture::new(
|
|
"project.arcadia",
|
|
source.clone(),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
let start = source.find(&literal).unwrap();
|
|
fixture.declarations[0].identity = Some(AuthoredIdentity {
|
|
id: expected,
|
|
literal_span: Span {
|
|
start,
|
|
end: start + literal.len(),
|
|
},
|
|
});
|
|
let admitted = index(&[fixture.included()]);
|
|
assert_eq!(anchor(&admitted, definition(&admitted, "a")).id(), expected);
|
|
assert_eq!(
|
|
admitted.source(0).unwrap().text.as_bytes(),
|
|
source.as_bytes()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn admitted_unicode_path_budget_keeps_original_policy_after_single_fold() {
|
|
let fixture = Fixture::new("İ.arcadia", String::new(), &[]);
|
|
assert!(fixture.path.to_lowercase().len() > fixture.path.len());
|
|
let exact = Limits {
|
|
files: 1,
|
|
source_bytes: 0,
|
|
path_bytes: fixture.path.len(),
|
|
tokens: 0,
|
|
records: 0,
|
|
};
|
|
let admitted = Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
exact,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(admitted.source(0).unwrap().path, "İ.arcadia");
|
|
// PathBytes continues to count authored input. One lowercase key's
|
|
// Unicode expansion is bounded scratch, not a new path rejection rule.
|
|
}
|
|
|
|
#[test]
|
|
fn every_budget_admits_boundary_and_returns_no_index_one_below() {
|
|
let fixture = Fixture::new(
|
|
"project.arcadia",
|
|
"oa_actor a \"Text\" {}\nsource @a; target @a;".into(),
|
|
&[("oa_actor", "a", None)],
|
|
);
|
|
let exact = Limits {
|
|
files: 1,
|
|
source_bytes: fixture.text.len(),
|
|
path_bytes: fixture.path.len(),
|
|
tokens: fixture.tokens.len(),
|
|
records: 3,
|
|
};
|
|
assert!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
exact
|
|
)
|
|
.is_ok()
|
|
);
|
|
for resource in [
|
|
Resource::Files,
|
|
Resource::SourceBytes,
|
|
Resource::PathBytes,
|
|
Resource::Tokens,
|
|
Resource::Records,
|
|
] {
|
|
let mut reduced = exact;
|
|
match resource {
|
|
Resource::Files => reduced.files -= 1,
|
|
Resource::SourceBytes => reduced.source_bytes -= 1,
|
|
Resource::PathBytes => reduced.path_bytes -= 1,
|
|
Resource::Tokens => reduced.tokens -= 1,
|
|
Resource::Records => reduced.records -= 1,
|
|
}
|
|
assert!(
|
|
matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, reduced), Err(BuildError::Limit { resource: actual, .. }) if actual == resource)
|
|
);
|
|
}
|
|
// Aggregate bytes/tokens/records, not a per-file budget reset.
|
|
let second = Fixture::new("other.arcadia", "source @a;".into(), &[]);
|
|
let mut aggregate = limits();
|
|
aggregate.records = 3;
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included(), second.included()],
|
|
Scope::CompleteProject,
|
|
aggregate
|
|
),
|
|
Err(BuildError::Limit {
|
|
resource: Resource::Records,
|
|
observed_at_least: 4,
|
|
..
|
|
})
|
|
));
|
|
let mut maximum = usize::MAX;
|
|
assert!(matches!(
|
|
charge(&mut maximum, 1, usize::MAX, Resource::SourceBytes),
|
|
Err(BuildError::Limit {
|
|
resource: Resource::SourceBytes,
|
|
..
|
|
})
|
|
));
|
|
assert_eq!(maximum, usize::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn declaration_order_and_duplicate_roles_refuse_instead_of_corrupting_hit_search() {
|
|
let mut fixture = Fixture::new(
|
|
"project.arcadia",
|
|
"oa_actor a \"First\" {}\noa_actor b \"Second\" {}".into(),
|
|
&[("oa_actor", "a", None), ("oa_actor", "b", None)],
|
|
);
|
|
fixture.declarations.reverse();
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DeclarationOrder,
|
|
..
|
|
})
|
|
));
|
|
fixture.declarations.reverse();
|
|
fixture.declarations.insert(1, fixture.declarations[0]);
|
|
assert!(matches!(
|
|
Index::build(
|
|
snapshot(1),
|
|
&[fixture.included()],
|
|
Scope::CompleteProject,
|
|
limits()
|
|
),
|
|
Err(BuildError::Input {
|
|
problem: InputProblem::DeclarationOrder,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
}
|