use crate::mib::{Module as SemanticModule, SemanticSpan, SemanticSpanKind};
use crate::source::{
ByteOffset, Position, PositionEncoding, PositionError, SourceDocument, SourceRange,
SourceRangeError,
};
use super::{CursorContext, SyntaxKind, SyntaxTree};
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum SourcePairError {
#[error("the resolved module does not retain a source document")]
SemanticSourceUnavailable,
#[error("the syntax tree and resolved module describe different source documents")]
MismatchedDocuments,
}
#[derive(Clone, Copy, Debug)]
pub struct LocatedRange<'src> {
document: &'src SourceDocument,
range: SourceRange,
}
impl<'src> LocatedRange<'src> {
fn new(document: &'src SourceDocument, range: SourceRange) -> Self {
debug_assert_eq!(document.id(), range.source());
Self { document, range }
}
pub fn document(self) -> &'src SourceDocument {
self.document
}
pub fn range(self) -> SourceRange {
self.range
}
pub fn text(self) -> Result<&'src [u8], SourceRangeError> {
self.document.slice(self.range)
}
}
#[derive(Clone, Copy, Debug)]
pub struct SymbolAtPosition<'tree, 'mib> {
pub syntax: CursorContext<'tree, 'tree>,
pub semantic: Option<SemanticSpan<'mib>>,
syntax_document: &'tree SourceDocument,
semantic_document: Option<&'mib SourceDocument>,
}
impl<'tree, 'mib> SymbolAtPosition<'tree, 'mib> {
pub fn syntax_range(self) -> LocatedRange<'tree> {
LocatedRange::new(self.syntax_document, self.syntax.token().range())
}
pub fn semantic_range(self) -> Option<LocatedRange<'mib>> {
Some(LocatedRange::new(
self.semantic_document?,
self.semantic?.range,
))
}
pub fn primary_range<'src>(self) -> LocatedRange<'src>
where
'tree: 'src,
'mib: 'src,
{
let Some(span) = self.semantic else {
let syntax = self.syntax_range();
return LocatedRange::new(syntax.document(), syntax.range());
};
let document = self
.semantic_document
.expect("a semantic span has its paired retained source");
let range = primary_semantic_range(document, span);
LocatedRange::new(document, range)
}
}
#[derive(Clone, Copy, Debug)]
pub struct SymbolNavigator<'tree, 'mib> {
tree: &'tree SyntaxTree,
module: Option<SemanticModule<'mib>>,
}
impl<'tree, 'mib> SymbolNavigator<'tree, 'mib> {
pub fn new(
tree: &'tree SyntaxTree,
module: Option<SemanticModule<'mib>>,
) -> Result<Self, SourcePairError> {
if let Some(module) = module {
let semantic = module
.source()
.ok_or(SourcePairError::SemanticSourceUnavailable)?;
let syntax = tree.document();
if syntax.origin() != semantic.origin() || syntax.bytes() != semantic.bytes() {
return Err(SourcePairError::MismatchedDocuments);
}
}
Ok(Self { tree, module })
}
pub fn tree(self) -> &'tree SyntaxTree {
self.tree
}
pub fn module(self) -> Option<SemanticModule<'mib>> {
self.module
}
pub fn symbol_at(self, offset: ByteOffset) -> Option<SymbolAtPosition<'tree, 'mib>> {
let syntax = self.tree.cursor_context(offset)?;
let semantic = self.module.and_then(|module| {
if suppress_semantics(syntax) {
return None;
}
module.semantic_at(offset)
});
Some(SymbolAtPosition {
syntax,
semantic,
syntax_document: self.tree.document(),
semantic_document: self.module.and_then(SemanticModule::source),
})
}
pub fn symbol_at_position(
self,
position: Position,
encoding: PositionEncoding,
) -> Result<Option<SymbolAtPosition<'tree, 'mib>>, PositionError> {
let offset = self.tree.document().position_offset(position, encoding)?;
Ok(self.symbol_at(offset))
}
}
fn suppress_semantics(context: CursorContext<'_, '_>) -> bool {
matches!(
context.token().kind(),
SyntaxKind::Comment
| SyntaxKind::QuotedString
| SyntaxKind::HexString
| SyntaxKind::BinString
)
}
fn primary_semantic_range(document: &SourceDocument, span: SemanticSpan<'_>) -> SourceRange {
assert_eq!(
document.id(),
span.range.source(),
"a semantic span belongs to its module source"
);
if span.kind != SemanticSpanKind::Definition {
return span.range;
}
let start = span.range.start().as_usize();
let end = start
.checked_add(span.declared_name.len())
.expect("a retained definition name fits the source coordinate space");
assert!(
end <= span.range.end().as_usize(),
"a retained definition name is inside its definition span"
);
let range = document
.range(start..end)
.expect("a retained definition span belongs to its module source");
assert_eq!(
document
.slice(range)
.expect("the narrowed definition range belongs to its module source"),
span.declared_name.as_bytes(),
"a retained definition span starts with its declared name"
);
range
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::cst;
use crate::mib::{Mib, ModuleData};
use crate::source::{SourceCandidate, SourceOrigin};
#[test]
fn source_less_semantic_module_is_rejected_before_lookup() {
let candidate = SourceCandidate::new(
"syntax",
SourceOrigin::memory("syntax"),
"syntax",
Arc::<[u8]>::from(b"SYNTAX-MIB DEFINITIONS ::= BEGIN END".as_slice()),
);
let (tree, _) = cst::parse(candidate).unwrap();
let mut mib = Mib::new();
mib.add_module(ModuleData::new("GENERATED-MIB".into()));
let module = mib.module("GENERATED-MIB").unwrap();
assert_eq!(
SymbolNavigator::new(&tree, Some(module)).unwrap_err(),
SourcePairError::SemanticSourceUnavailable
);
}
}