1use super::{
8 CaptureBound, CaptureBuildRefusal, CaptureBuilder, CaptureLevel, CapturedAtom,
9 CapturedDelimiter, CapturedInput, LiteralReadCause, SpanHandle, SpanTable,
10 TEXT_SOURCE_BYTE_LIMIT, TextCapture, TextLexicalCause, TextReadCause, TextReadRefusal,
11 TokenPath, capture_literal,
12};
13use crate::bounded::Bounded;
14use crate::token::bank::raw_identifier_is_reserved;
15use ra_ap_rustc_lexer::{DocStyle, FrontmatterAllowed, LiteralKind, TokenKind, tokenize};
16
17impl TextCapture {
18 pub fn read(source: &str) -> Result<Self, TextReadRefusal> {
25 if source.len() > TEXT_SOURCE_BYTE_LIMIT {
26 return Err(TextReadRefusal {
27 cause: TextReadCause::SourceBytesUnbounded,
28 at: source_offset(TEXT_SOURCE_BYTE_LIMIT),
29 });
30 }
31 let lexemes = lex(source)?;
32 let mut builder = CaptureBuilder::declared();
33 let mut cursor = 0;
34 let level = builder.open();
35 let level = capture_sequence(&lexemes, &mut cursor, None, level)
36 .map_err(|refusal| text_refusal(&refusal))?;
37 let input = level.finish();
38 let offsets = Bounded::new(builder.positions().to_vec()).map_err(|_| TextReadRefusal {
39 cause: TextReadCause::Unbounded(CaptureBound::Tree),
40 at: source_offset(source.len()),
41 })?;
42 Ok(Self {
43 input,
44 spans: SpanTable::ByteOffsets(offsets),
45 })
46 }
47
48 #[must_use]
50 pub const fn input(&self) -> &CapturedInput {
51 &self.input
52 }
53
54 #[must_use]
56 pub const fn spans(&self) -> &SpanTable {
57 &self.spans
58 }
59}
60
61#[derive(Debug, Clone, Copy)]
63struct Lexeme<'source> {
64 kind: TokenKind,
65 spelling: &'source str,
66 at: u64,
67}
68
69struct Closing {
71 kind: ClosingKind,
72 at: u64,
73 path: TokenPath,
74 span: SpanHandle,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79enum ClosingKind {
80 Parenthesis,
81 Brace,
82 Bracket,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87enum LifetimeForm {
88 Ordinary,
89 Raw,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94enum DocForm {
95 Line,
96 Block,
97}
98
99fn lex(source: &str) -> Result<Vec<Lexeme<'_>>, TextReadRefusal> {
101 let mut lexemes = Vec::new();
102 let mut offset = 0usize;
103 for token in tokenize(source, FrontmatterAllowed::No) {
104 let length = usize::try_from(token.len).map_err(|_| TextReadRefusal {
105 cause: TextReadCause::SourceBytesUnbounded,
106 at: source_offset(offset),
107 })?;
108 let end = offset.checked_add(length).ok_or(TextReadRefusal {
109 cause: TextReadCause::SourceBytesUnbounded,
110 at: source_offset(offset),
111 })?;
112 let spelling = source.get(offset..end).ok_or(TextReadRefusal {
113 cause: TextReadCause::Lexical(TextLexicalCause::UnknownToken),
114 at: source_offset(offset),
115 })?;
116 lexemes.push(Lexeme {
117 kind: token.kind,
118 spelling,
119 at: source_offset(offset),
120 });
121 offset = end;
122 }
123 if offset != source.len() {
124 return Err(TextReadRefusal {
125 cause: TextReadCause::Lexical(TextLexicalCause::UnknownToken),
126 at: source_offset(offset),
127 });
128 }
129 Ok(lexemes)
130}
131
132fn capture_sequence<'capture>(
134 lexemes: &[Lexeme<'_>],
135 cursor: &mut usize,
136 closing: Option<Closing>,
137 mut level: CaptureLevel<'capture, u64>,
138) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
139 loop {
140 let Some(lexeme) = lexemes.get(*cursor) else {
141 return match closing {
142 Some(held) => Err(CaptureBuildRefusal::ProducerRefused {
143 cause: refusal(TextReadCause::NotBalanced, held.at),
144 path: held.path,
145 at: held.span,
146 }),
147 None => Ok(level),
148 };
149 };
150 if closes(lexeme.kind, closing.as_ref()) {
151 advance(cursor);
152 return Ok(level);
153 }
154 if is_closing(lexeme.kind) {
155 return refuse(level, lexeme.at, TextReadCause::NotOpened);
156 }
157 if let Some((delimiter, expected)) = opening(lexeme.kind) {
158 advance(cursor);
159 level = level.group(lexeme.at, delimiter, |span, inner| {
160 let path = inner.path().clone();
161 capture_sequence(
162 lexemes,
163 cursor,
164 Some(Closing {
165 kind: expected,
166 at: lexeme.at,
167 path,
168 span,
169 }),
170 inner,
171 )
172 })?;
173 continue;
174 }
175 level = capture_lexeme(lexemes, cursor, level)?;
176 }
177}
178
179fn capture_lexeme<'capture>(
181 lexemes: &[Lexeme<'_>],
182 cursor: &mut usize,
183 level: CaptureLevel<'capture, u64>,
184) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
185 let Some(lexeme) = lexemes.get(*cursor) else {
186 return Ok(level);
187 };
188 let index = *cursor;
189 advance(cursor);
190 match lexeme.kind {
191 TokenKind::Whitespace | TokenKind::LineComment { doc_style: None } => {
192 level.examined(lexeme.at)
193 }
194 TokenKind::BlockComment {
195 doc_style: None,
196 terminated: true,
197 } => level.examined(lexeme.at),
198 TokenKind::BlockComment {
199 doc_style: _,
200 terminated: false,
201 } => refuse(
202 level,
203 lexeme.at,
204 TextReadCause::Lexical(TextLexicalCause::BlockCommentNotTerminated),
205 ),
206 TokenKind::LineComment {
207 doc_style: Some(style),
208 } => capture_doc(level, lexeme, style, DocForm::Line),
209 TokenKind::BlockComment {
210 doc_style: Some(style),
211 terminated: true,
212 } => capture_doc(level, lexeme, style, DocForm::Block),
213 TokenKind::Ident => capture_atom(
214 level,
215 lexeme.at,
216 CapturedAtom::Word(lexeme.spelling.to_owned()),
217 ),
218 TokenKind::RawIdent => capture_prefixed_identifier(level, lexeme, "r#"),
219 TokenKind::Lifetime {
220 starts_with_number: false,
221 } => capture_lifetime(level, lexeme, LifetimeForm::Ordinary),
222 TokenKind::RawLifetime => capture_lifetime(level, lexeme, LifetimeForm::Raw),
223 TokenKind::Literal { kind, suffix_start } => {
224 capture_lexed_literal(level, lexeme, kind, suffix_start)
225 }
226 TokenKind::Semi => capture_punctuation(level, lexemes, index, ';'),
227 TokenKind::Comma => capture_punctuation(level, lexemes, index, ','),
228 TokenKind::Dot => capture_punctuation(level, lexemes, index, '.'),
229 TokenKind::At => capture_punctuation(level, lexemes, index, '@'),
230 TokenKind::Pound => capture_punctuation(level, lexemes, index, '#'),
231 TokenKind::Tilde => capture_punctuation(level, lexemes, index, '~'),
232 TokenKind::Question => capture_punctuation(level, lexemes, index, '?'),
233 TokenKind::Colon => capture_punctuation(level, lexemes, index, ':'),
234 TokenKind::Dollar => capture_punctuation(level, lexemes, index, '$'),
235 TokenKind::Eq => capture_punctuation(level, lexemes, index, '='),
236 TokenKind::Bang => capture_punctuation(level, lexemes, index, '!'),
237 TokenKind::Lt => capture_punctuation(level, lexemes, index, '<'),
238 TokenKind::Gt => capture_punctuation(level, lexemes, index, '>'),
239 TokenKind::Minus => capture_punctuation(level, lexemes, index, '-'),
240 TokenKind::And => capture_punctuation(level, lexemes, index, '&'),
241 TokenKind::Or => capture_punctuation(level, lexemes, index, '|'),
242 TokenKind::Plus => capture_punctuation(level, lexemes, index, '+'),
243 TokenKind::Star => capture_punctuation(level, lexemes, index, '*'),
244 TokenKind::Slash => capture_punctuation(level, lexemes, index, '/'),
245 TokenKind::Caret => capture_punctuation(level, lexemes, index, '^'),
246 TokenKind::Percent => capture_punctuation(level, lexemes, index, '%'),
247 TokenKind::InvalidIdent => {
248 lexical_refusal(level, lexeme, TextLexicalCause::InvalidIdentifier)
249 }
250 TokenKind::UnknownPrefix => lexical_refusal(level, lexeme, TextLexicalCause::UnknownPrefix),
251 TokenKind::UnknownPrefixLifetime => {
252 lexical_refusal(level, lexeme, TextLexicalCause::UnknownLifetimePrefix)
253 }
254 TokenKind::GuardedStrPrefix => {
255 lexical_refusal(level, lexeme, TextLexicalCause::GuardedStringPrefix)
256 }
257 TokenKind::Lifetime {
258 starts_with_number: true,
259 } => lexical_refusal(level, lexeme, TextLexicalCause::LifetimeStartsWithNumber),
260 TokenKind::Frontmatter {
261 has_invalid_preceding_whitespace: _,
262 invalid_infostring: _,
263 } => lexical_refusal(level, lexeme, TextLexicalCause::Frontmatter),
264 TokenKind::Unknown
265 | TokenKind::OpenParen
266 | TokenKind::CloseParen
267 | TokenKind::OpenBrace
268 | TokenKind::CloseBrace
269 | TokenKind::OpenBracket
270 | TokenKind::CloseBracket => lexical_refusal(level, lexeme, TextLexicalCause::UnknownToken),
271 TokenKind::Eof => Ok(level),
272 }
273}
274
275fn capture_lexed_literal<'capture>(
277 level: CaptureLevel<'capture, u64>,
278 lexeme: &Lexeme<'_>,
279 kind: LiteralKind,
280 suffix_start: u32,
281) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
282 let suffix = usize::try_from(suffix_start)
283 .ok()
284 .and_then(|start| lexeme.spelling.get(start..));
285 if let Some(cause) = malformed_literal(kind) {
286 return refuse(level, lexeme.at, cause);
287 }
288 if suffix == Some("_") {
289 return lexical_refusal(level, lexeme, TextLexicalCause::MalformedLiteral);
290 }
291 level.atom(lexeme.at, |_| {
292 capture_literal(lexeme.spelling).map_err(|cause| match cause {
293 LiteralReadCause::NotReadable if lexeme.spelling.contains('\\') => {
294 refusal(TextReadCause::NotEscapeFree, lexeme.at)
295 }
296 LiteralReadCause::NotAKnownForm | LiteralReadCause::NotReadable => refusal(
297 TextReadCause::Lexical(TextLexicalCause::MalformedLiteral),
298 lexeme.at,
299 ),
300 })
301 })
302}
303
304const fn malformed_literal(kind: LiteralKind) -> Option<TextReadCause> {
306 match kind {
307 LiteralKind::Int { base: _, empty_int } => {
308 if empty_int {
309 Some(TextReadCause::Lexical(TextLexicalCause::MalformedLiteral))
310 } else {
311 None
312 }
313 }
314 LiteralKind::Float {
315 base: _,
316 empty_exponent,
317 } => {
318 if empty_exponent {
319 Some(TextReadCause::Lexical(TextLexicalCause::MalformedLiteral))
320 } else {
321 None
322 }
323 }
324 LiteralKind::Char { terminated }
325 | LiteralKind::Byte { terminated }
326 | LiteralKind::Str { terminated }
327 | LiteralKind::ByteStr { terminated }
328 | LiteralKind::CStr { terminated } => {
329 if terminated {
330 None
331 } else {
332 Some(TextReadCause::NotTerminated)
333 }
334 }
335 LiteralKind::RawStr { n_hashes }
336 | LiteralKind::RawByteStr { n_hashes }
337 | LiteralKind::RawCStr { n_hashes } => {
338 if n_hashes.is_none() {
339 Some(TextReadCause::Lexical(TextLexicalCause::MalformedLiteral))
340 } else {
341 None
342 }
343 }
344 }
345}
346
347fn capture_lifetime<'capture>(
349 level: CaptureLevel<'capture, u64>,
350 lexeme: &Lexeme<'_>,
351 form: LifetimeForm,
352) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
353 let prefix = match form {
354 LifetimeForm::Ordinary => "'",
355 LifetimeForm::Raw => "'r#",
356 };
357 let Some(name) = lexeme.spelling.strip_prefix(prefix) else {
358 return lexical_refusal(level, lexeme, TextLexicalCause::UnknownLifetimePrefix);
359 };
360 if form == LifetimeForm::Raw && raw_identifier_is_reserved(name) {
361 return lexical_refusal(level, lexeme, TextLexicalCause::InvalidIdentifier);
362 }
363 let level = capture_atom(level, lexeme.at, CapturedAtom::JointPunct('\''))?;
364 let atom = match form {
365 LifetimeForm::Ordinary => CapturedAtom::Word(name.to_owned()),
366 LifetimeForm::Raw => CapturedAtom::RawIdentifier(name.to_owned()),
367 };
368 capture_atom(level, lexeme.at, atom)
369}
370
371fn capture_prefixed_identifier<'capture>(
373 level: CaptureLevel<'capture, u64>,
374 lexeme: &Lexeme<'_>,
375 prefix: &str,
376) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
377 let Some(name) = lexeme.spelling.strip_prefix(prefix) else {
378 return lexical_refusal(level, lexeme, TextLexicalCause::InvalidIdentifier);
379 };
380 if raw_identifier_is_reserved(name) {
381 return lexical_refusal(level, lexeme, TextLexicalCause::InvalidIdentifier);
382 }
383 capture_atom(
384 level,
385 lexeme.at,
386 CapturedAtom::RawIdentifier(name.to_owned()),
387 )
388}
389
390fn capture_doc<'capture>(
392 level: CaptureLevel<'capture, u64>,
393 lexeme: &Lexeme<'_>,
394 style: DocStyle,
395 form: DocForm,
396) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
397 let Some(body) = doc_body(lexeme.spelling, style, form) else {
398 return lexical_refusal(level, lexeme, TextLexicalCause::UnknownToken);
399 };
400 let level = capture_atom(level, lexeme.at, CapturedAtom::Punct('#'))?;
401 let level = match style {
402 DocStyle::Outer => level,
403 DocStyle::Inner => capture_atom(level, lexeme.at, CapturedAtom::Punct('!'))?,
404 };
405 level.group(lexeme.at, CapturedDelimiter::Bracket, |_span, inner| {
406 let inner = capture_atom(inner, lexeme.at, CapturedAtom::Word("doc".to_owned()))?;
407 let inner = capture_atom(inner, lexeme.at, CapturedAtom::Punct('='))?;
408 capture_atom(inner, lexeme.at, CapturedAtom::Text(body.to_owned()))
409 })
410}
411
412fn doc_body(spelling: &str, style: DocStyle, form: DocForm) -> Option<&str> {
414 let prefix = match (style, form) {
415 (DocStyle::Outer, DocForm::Line) => "///",
416 (DocStyle::Inner, DocForm::Line) => "//!",
417 (DocStyle::Outer, DocForm::Block) => "/**",
418 (DocStyle::Inner, DocForm::Block) => "/*!",
419 };
420 let body = spelling.strip_prefix(prefix)?;
421 match form {
422 DocForm::Line => Some(body),
423 DocForm::Block => body.strip_suffix("*/"),
424 }
425}
426
427fn capture_punctuation<'capture>(
429 level: CaptureLevel<'capture, u64>,
430 lexemes: &[Lexeme<'_>],
431 index: usize,
432 mark: char,
433) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
434 let at = lexemes.get(index).map_or(u64::MAX, |lexeme| lexeme.at);
435 let atom = if punctuation_is_joint(lexemes, index) {
436 CapturedAtom::JointPunct(mark)
437 } else {
438 CapturedAtom::Punct(mark)
439 };
440 capture_atom(level, at, atom)
441}
442
443fn punctuation_is_joint(lexemes: &[Lexeme<'_>], index: usize) -> bool {
445 lexemes
446 .get(index.saturating_add(1))
447 .is_some_and(|lexeme| is_punctuation(lexeme.kind))
448}
449
450const fn is_punctuation(kind: TokenKind) -> bool {
452 matches!(
453 kind,
454 TokenKind::Semi
455 | TokenKind::Comma
456 | TokenKind::Dot
457 | TokenKind::At
458 | TokenKind::Pound
459 | TokenKind::Tilde
460 | TokenKind::Question
461 | TokenKind::Colon
462 | TokenKind::Dollar
463 | TokenKind::Eq
464 | TokenKind::Bang
465 | TokenKind::Lt
466 | TokenKind::Gt
467 | TokenKind::Minus
468 | TokenKind::And
469 | TokenKind::Or
470 | TokenKind::Plus
471 | TokenKind::Star
472 | TokenKind::Slash
473 | TokenKind::Caret
474 | TokenKind::Percent
475 )
476}
477
478fn capture_atom(
480 level: CaptureLevel<'_, u64>,
481 at: u64,
482 atom: CapturedAtom,
483) -> Result<CaptureLevel<'_, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
484 level.atom(at, |_| Ok(atom))
485}
486
487fn lexical_refusal<'capture>(
489 level: CaptureLevel<'capture, u64>,
490 lexeme: &Lexeme<'_>,
491 cause: TextLexicalCause,
492) -> Result<CaptureLevel<'capture, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
493 refuse(level, lexeme.at, TextReadCause::Lexical(cause))
494}
495
496fn refuse(
498 level: CaptureLevel<'_, u64>,
499 at: u64,
500 cause: TextReadCause,
501) -> Result<CaptureLevel<'_, u64>, CaptureBuildRefusal<u64, TextReadRefusal>> {
502 level.atom(at, |_| Err(refusal(cause, at)))
503}
504
505const fn refusal(cause: TextReadCause, at: u64) -> TextReadRefusal {
507 TextReadRefusal { cause, at }
508}
509
510fn text_refusal(refusal: &CaptureBuildRefusal<u64, TextReadRefusal>) -> TextReadRefusal {
512 match refusal {
513 CaptureBuildRefusal::Unbounded { bound, at } => TextReadRefusal {
514 cause: TextReadCause::Unbounded(*bound),
515 at: *at,
516 },
517 CaptureBuildRefusal::ProducerRefused {
518 cause,
519 path: _,
520 at: _,
521 } => *cause,
522 }
523}
524
525fn advance(cursor: &mut usize) {
527 *cursor = cursor.saturating_add(1);
528}
529
530fn source_offset(offset: usize) -> u64 {
532 u64::try_from(offset).unwrap_or(u64::MAX)
533}
534
535const fn opening(kind: TokenKind) -> Option<(CapturedDelimiter, ClosingKind)> {
537 match kind {
538 TokenKind::OpenParen => Some((CapturedDelimiter::Parenthesis, ClosingKind::Parenthesis)),
539 TokenKind::OpenBrace => Some((CapturedDelimiter::Brace, ClosingKind::Brace)),
540 TokenKind::OpenBracket => Some((CapturedDelimiter::Bracket, ClosingKind::Bracket)),
541 TokenKind::LineComment { .. }
542 | TokenKind::BlockComment { .. }
543 | TokenKind::Whitespace
544 | TokenKind::Frontmatter { .. }
545 | TokenKind::Ident
546 | TokenKind::InvalidIdent
547 | TokenKind::RawIdent
548 | TokenKind::UnknownPrefix
549 | TokenKind::UnknownPrefixLifetime
550 | TokenKind::RawLifetime
551 | TokenKind::GuardedStrPrefix
552 | TokenKind::Literal { .. }
553 | TokenKind::Lifetime { .. }
554 | TokenKind::Semi
555 | TokenKind::Comma
556 | TokenKind::Dot
557 | TokenKind::CloseParen
558 | TokenKind::CloseBrace
559 | TokenKind::CloseBracket
560 | TokenKind::At
561 | TokenKind::Pound
562 | TokenKind::Tilde
563 | TokenKind::Question
564 | TokenKind::Colon
565 | TokenKind::Dollar
566 | TokenKind::Eq
567 | TokenKind::Bang
568 | TokenKind::Lt
569 | TokenKind::Gt
570 | TokenKind::Minus
571 | TokenKind::And
572 | TokenKind::Or
573 | TokenKind::Plus
574 | TokenKind::Star
575 | TokenKind::Slash
576 | TokenKind::Caret
577 | TokenKind::Percent
578 | TokenKind::Unknown
579 | TokenKind::Eof => None,
580 }
581}
582
583const fn is_closing(kind: TokenKind) -> bool {
585 matches!(
586 kind,
587 TokenKind::CloseParen | TokenKind::CloseBrace | TokenKind::CloseBracket
588 )
589}
590
591fn closes(kind: TokenKind, closing: Option<&Closing>) -> bool {
593 match (kind, closing.map(|held| held.kind)) {
594 (TokenKind::CloseParen, Some(ClosingKind::Parenthesis))
595 | (TokenKind::CloseBrace, Some(ClosingKind::Brace))
596 | (TokenKind::CloseBracket, Some(ClosingKind::Bracket)) => true,
597 (
598 TokenKind::LineComment { .. }
599 | TokenKind::BlockComment { .. }
600 | TokenKind::Whitespace
601 | TokenKind::Frontmatter { .. }
602 | TokenKind::Ident
603 | TokenKind::InvalidIdent
604 | TokenKind::RawIdent
605 | TokenKind::UnknownPrefix
606 | TokenKind::UnknownPrefixLifetime
607 | TokenKind::RawLifetime
608 | TokenKind::GuardedStrPrefix
609 | TokenKind::Literal { .. }
610 | TokenKind::Lifetime { .. }
611 | TokenKind::Semi
612 | TokenKind::Comma
613 | TokenKind::Dot
614 | TokenKind::OpenParen
615 | TokenKind::OpenBrace
616 | TokenKind::OpenBracket
617 | TokenKind::CloseParen
618 | TokenKind::CloseBrace
619 | TokenKind::CloseBracket
620 | TokenKind::At
621 | TokenKind::Pound
622 | TokenKind::Tilde
623 | TokenKind::Question
624 | TokenKind::Colon
625 | TokenKind::Dollar
626 | TokenKind::Eq
627 | TokenKind::Bang
628 | TokenKind::Lt
629 | TokenKind::Gt
630 | TokenKind::Minus
631 | TokenKind::And
632 | TokenKind::Or
633 | TokenKind::Plus
634 | TokenKind::Star
635 | TokenKind::Slash
636 | TokenKind::Caret
637 | TokenKind::Percent
638 | TokenKind::Unknown
639 | TokenKind::Eof,
640 None | Some(ClosingKind::Parenthesis | ClosingKind::Brace | ClosingKind::Bracket),
641 ) => false,
642 }
643}