use crate::diagnostic::{Diagnostic, DiagnosticCode};
use crate::parser::Parser;
use alloc::string::ToString;
use alloc::vec::Vec;
use zerodds_idl_cst::{CstKind, Span, TokenKind, Trivia, TriviaKind, lex};
pub(crate) const MARKER: &str = "//@";
#[must_use]
pub(crate) fn is_suffix_comment(text: &str) -> bool {
text.starts_with(MARKER)
}
impl Parser<'_> {
pub(crate) fn parse_suffix_annotations(&mut self) {
while let Some(count) = self.suffix_run_length() {
self.take_suffix_comment(count);
}
}
fn suffix_run_length(&self) -> Option<usize> {
let token = self.tokens.get(self.at)?;
for (index, trivia) in token.leading.iter().enumerate() {
match trivia.kind {
TriviaKind::Whitespace | TriviaKind::Newline => {}
TriviaKind::LineComment
if is_suffix_comment(trivia.span.slice(self.source).unwrap_or("")) =>
{
return Some(index + 1);
}
_ => return None,
}
}
None
}
fn take_suffix_comment(&mut self, count: usize) {
let Some(token) = self.tokens.get_mut(self.at) else {
return;
};
let taken: Vec<Trivia> = token.leading.drain(..count).collect();
let Some((comment, before)) = taken.split_last() else {
return;
};
for trivia in before {
let text = trivia.span.slice(self.source).unwrap_or("");
self.emit(crate::parser::trivia_token(trivia.kind), text, trivia.span);
}
for segment in self.suffix_segments(comment.span) {
self.start(CstKind::Annotation);
#[allow(clippy::cast_possible_truncation)]
let marker = Span::new(
segment.start.saturating_sub(MARKER.len() as u32),
segment.start,
);
self.emit(TokenKind::SlashSlashAt, MARKER, marker);
self.parse_suffix_body(segment);
self.finish();
}
}
fn suffix_segments(&self, comment: Span) -> Vec<Span> {
let text = comment.slice(self.source).unwrap_or("");
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut start = MARKER.len();
let mut at = start;
let mut quote: Option<u8> = None;
while at < bytes.len() {
let byte = bytes[at];
match quote {
Some(closing) => {
if byte == b'\\' {
at += 2;
continue;
}
if byte == closing {
quote = None;
}
at += 1;
}
None if byte == b'"' || byte == b'\'' => {
quote = Some(byte);
at += 1;
}
None if bytes[at..].starts_with(MARKER.as_bytes()) => {
out.push(self.sub_span(comment, start, at));
at += MARKER.len();
start = at;
}
None => at += 1,
}
}
out.push(self.sub_span(comment, start, bytes.len()));
out
}
#[allow(clippy::cast_possible_truncation)]
fn sub_span(&self, comment: Span, from: usize, to: usize) -> Span {
let base = comment.start;
Span::new(
base.saturating_add(from as u32),
base.saturating_add(to as u32),
)
}
fn parse_suffix_body(&mut self, body: Span) {
let Some(text) = body.slice(self.source) else {
return;
};
let mut tokens = lex(text);
for token in &mut tokens {
token.span = shift(token.span, body.start);
for trivia in &mut token.leading {
trivia.span = shift(trivia.span, body.start);
}
}
let outer_tokens = core::mem::replace(&mut self.tokens, tokens);
let outer_at = core::mem::replace(&mut self.at, 0);
self.parse_annotation_tail();
if !self.at_eof() {
self.diagnose(
DiagnosticCode::UnexpectedToken,
"a `//@` annotation ends with the comment; nothing may follow it on the line",
"DDS-XTypes 1.3 §7.3.1.2.3",
);
self.start(CstKind::Error);
while !self.at_eof() {
self.bump();
}
self.finish();
}
self.eat_eof();
self.tokens = outer_tokens;
self.at = outer_at;
}
pub(crate) fn diagnose_stray_suffix(&mut self, span: Span) {
self.diagnostics.push(Diagnostic::error(
DiagnosticCode::UnexpectedToken,
span,
"a `//@` annotation may only follow the declaration it applies to".to_string(),
"DDS-XTypes 1.3 §7.3.1.2.5",
));
}
}
const fn shift(span: Span, by: u32) -> Span {
Span::new(span.start.saturating_add(by), span.end.saturating_add(by))
}