|
| 1 | +//! Helper functions for parsing common patterns. |
| 2 | +//! |
| 3 | +//! Utilities shared across parser components, primarily for associating |
| 4 | +//! documentation comments with subsequent declarations. Spans are preserved |
| 5 | +//! and comments are treated according to the scanner’s guarantees. |
| 6 | +
|
| 7 | +use crate::core::parser::ast::Docs; |
| 8 | +use crate::core::parser::traits::TokenStream; |
| 9 | +use crate::core::scanner::tokens::{ |
| 10 | + SymbolLocation, SymbolSpan, Token, TokenType, |
| 11 | +}; |
| 12 | + |
| 13 | +/// Build a span that goes from the start of `a` to the end of `b`. |
| 14 | +pub(crate) fn span_from_to(a: &SymbolSpan, b: &SymbolSpan) -> SymbolSpan { |
| 15 | + SymbolSpan { |
| 16 | + start: SymbolLocation { |
| 17 | + line: a.start.line, |
| 18 | + column: a.start.column, |
| 19 | + }, |
| 20 | + end: SymbolLocation { |
| 21 | + line: b.end.line, |
| 22 | + column: b.end.column, |
| 23 | + }, |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +/// Extract documentation text from a `DocComment` token. |
| 28 | +/// |
| 29 | +/// Normalizes the raw doc comment text by stripping an optional leading |
| 30 | +/// `///` prefix and trimming surrounding whitespace. Works for inputs with |
| 31 | +/// or without the `///` prefix. |
| 32 | +#[must_use] |
| 33 | +pub fn extract_doc_text(token: &Token) -> Option<String> { |
| 34 | + if let TokenType::DocComment(text) = token.r#type() { |
| 35 | + let s = text.strip_prefix("///").unwrap_or(text).trim(); |
| 36 | + Some(s.to_string()) |
| 37 | + } else { |
| 38 | + None |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +/// Parse leading documentation comments from the stream. |
| 43 | +/// |
| 44 | +/// Greedily consumes consecutive `DocComment` tokens at the current position |
| 45 | +/// and returns a single `Docs` node with the collected lines and combined |
| 46 | +/// span. If no doc comments are present, returns `None` and does not |
| 47 | +/// advance the stream. |
| 48 | +/// |
| 49 | +/// ## Examples |
| 50 | +/// ``` |
| 51 | +/// # use prisma_rs::core::parser::components::helpers::parse_leading_docs; |
| 52 | +/// # use prisma_rs::core::parser::stream::VectorTokenStream; |
| 53 | +/// # use prisma_rs::core::parser::traits::TokenStream; |
| 54 | +/// # use prisma_rs::core::scanner::tokens::{Token, TokenType}; |
| 55 | +/// let mut s = VectorTokenStream::new(vec![ |
| 56 | +/// Token::new(TokenType::DocComment("User model".into()), (1,1), (1,1)), |
| 57 | +/// Token::new(TokenType::DocComment("for auth".into()), (1,2), (1,2)), |
| 58 | +/// Token::new(TokenType::Model, (1,3), (1,7)), |
| 59 | +/// ]); |
| 60 | +/// let docs = parse_leading_docs(&mut s).expect("should collect docs"); |
| 61 | +/// assert_eq!(docs.lines, vec!["User model", "for auth"]); |
| 62 | +/// assert!(matches!(s.peek().unwrap().r#type(), TokenType::Model)); |
| 63 | +/// ``` |
| 64 | +pub fn parse_leading_docs(stream: &mut dyn TokenStream) -> Option<Docs> { |
| 65 | + let mut doc_lines = Vec::new(); |
| 66 | + let mut first_span: Option<SymbolSpan> = None; |
| 67 | + let mut last_span: Option<SymbolSpan> = None; |
| 68 | + |
| 69 | + // Greedily consume all consecutive DocComment tokens |
| 70 | + while let Some(token) = stream.peek() { |
| 71 | + if matches!(token.r#type(), TokenType::DocComment(_)) { |
| 72 | + // Safe to consume since we peeked and confirmed it's a DocComment |
| 73 | + // This should never panic since we just checked with peek() |
| 74 | + if let Some(doc_token) = stream.next() { |
| 75 | + // Track spans for the overall Docs span |
| 76 | + if first_span.is_none() { |
| 77 | + first_span = Some(doc_token.span().clone()); |
| 78 | + } |
| 79 | + last_span = Some(doc_token.span().clone()); |
| 80 | + |
| 81 | + // Extract and store the documentation text |
| 82 | + if let Some(doc_text) = extract_doc_text(&doc_token) { |
| 83 | + doc_lines.push(doc_text); |
| 84 | + } |
| 85 | + } |
| 86 | + } else { |
| 87 | + // Hit a non-DocComment token, stop consuming |
| 88 | + break; |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + // If we found any documentation comments, create a Docs node |
| 93 | + if doc_lines.is_empty() { |
| 94 | + None |
| 95 | + } else { |
| 96 | + let span = match (first_span, last_span) { |
| 97 | + (Some(first), Some(last)) => span_from_to(&first, &last), |
| 98 | + (Some(single), None) => single, // Should not happen, but handle gracefully |
| 99 | + _ => { |
| 100 | + // This should never happen if doc_lines is not empty |
| 101 | + SymbolSpan { |
| 102 | + start: SymbolLocation { line: 0, column: 0 }, |
| 103 | + end: SymbolLocation { line: 0, column: 0 }, |
| 104 | + } |
| 105 | + } |
| 106 | + }; |
| 107 | + |
| 108 | + Some(Docs { |
| 109 | + lines: doc_lines, |
| 110 | + span, |
| 111 | + }) |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +#[cfg(test)] |
| 116 | +mod tests { |
| 117 | + #![expect(clippy::unwrap_used)] |
| 118 | + |
| 119 | + use super::*; |
| 120 | + use crate::core::parser::stream::VectorTokenStream; |
| 121 | + |
| 122 | + fn tok(t: TokenType) -> Token { |
| 123 | + Token::new(t, (1, 1), (1, 1)) |
| 124 | + } |
| 125 | + |
| 126 | + #[test] |
| 127 | + fn extract_doc_text_variants() { |
| 128 | + let t = tok(TokenType::DocComment("/// hello".into())); |
| 129 | + assert_eq!(extract_doc_text(&t).unwrap(), "hello"); |
| 130 | + let t = tok(TokenType::DocComment("plain".into())); |
| 131 | + assert_eq!(extract_doc_text(&t).unwrap(), "plain"); |
| 132 | + let t = tok(TokenType::Comment(" not-doc".into())); |
| 133 | + assert!(extract_doc_text(&t).is_none()); |
| 134 | + } |
| 135 | + |
| 136 | + #[test] |
| 137 | + fn parse_leading_docs_none_and_some() { |
| 138 | + // None path (no docs) |
| 139 | + let mut s = VectorTokenStream::new(vec![tok(TokenType::Model)]); |
| 140 | + assert!(parse_leading_docs(&mut s).is_none()); |
| 141 | + |
| 142 | + // Some path with multiple lines |
| 143 | + let mut s = VectorTokenStream::new(vec![ |
| 144 | + tok(TokenType::DocComment(" line1".into())), |
| 145 | + tok(TokenType::DocComment(" line2".into())), |
| 146 | + tok(TokenType::Enum), |
| 147 | + ]); |
| 148 | + let d = parse_leading_docs(&mut s).unwrap(); |
| 149 | + assert_eq!(d.lines, vec!["line1", "line2"]); |
| 150 | + assert!(matches!(s.peek().unwrap().r#type(), TokenType::Enum)); |
| 151 | + } |
| 152 | +} |
0 commit comments