| //! Parser that can take Rust source and generates a sequence of tokens. |
| |
| use super::{Node, ParseError}; |
| use crate::coverage::Coverage; |
| use crate::parser::{SourceIndex, parse_production}; |
| use grammar::{ExpressionKind, Grammar, Production}; |
| use tracing::debug; |
| |
| #[derive(Clone)] |
| pub struct Tokens { |
| pub tokens: Vec<Node>, |
| /// Byte range of the shebang. |
| /// |
| /// The reference lexer is the only tool that sets this. |
| pub shebang: Option<Node>, |
| /// Byte range of the frontmatter. |
| /// |
| /// The reference lexer is the only tool that sets this. |
| pub frontmatter: Option<Node>, |
| } |
| |
| pub fn tokenize( |
| grammar: &Grammar, |
| coverage: &mut Coverage, |
| original_src: &str, |
| ) -> Result<Tokens, ParseError> { |
| let (normalized_src, removed_indices) = normalize_crlf(original_src); |
| |
| tokenize_normalized(grammar, coverage, &normalized_src) |
| .map(|mut tokens| { |
| for token in &mut tokens.tokens { |
| adjust_node(&removed_indices, token); |
| } |
| if let Some(shebang) = &mut tokens.shebang { |
| adjust_node(&removed_indices, shebang); |
| } |
| if let Some(frontmatter) = &mut tokens.frontmatter { |
| adjust_node(&removed_indices, frontmatter); |
| } |
| tokens |
| }) |
| .map_err(|mut err| { |
| err.byte_offset = map_offset(&removed_indices, err.byte_offset); |
| err |
| }) |
| } |
| |
| fn normalize_crlf(src: &str) -> (String, Vec<usize>) { |
| let mut normalized_src = String::with_capacity(src.len()); |
| let mut removed_indices = Vec::new(); |
| let mut chars = src.chars().peekable(); |
| while let Some(ch) = chars.next() { |
| if ch == '\r' |
| && let Some(&'\n') = chars.peek() |
| { |
| removed_indices.push(normalized_src.len()); |
| continue; |
| } |
| normalized_src.push(ch); |
| } |
| (normalized_src, removed_indices) |
| } |
| |
| fn map_offset(removed_indices: &[usize], offset: usize) -> usize { |
| offset + removed_indices.partition_point(|&x| x < offset) |
| } |
| |
| /// Adjusts the node range for CRLF normalization so that the range matches |
| /// the original source with the carriage returns. |
| fn adjust_node(removed_indices: &[usize], node: &mut Node) { |
| node.range.start = map_offset(removed_indices, node.range.start); |
| node.range.end = map_offset(removed_indices, node.range.end); |
| for child in &mut node.children.0 { |
| adjust_node(removed_indices, child); |
| } |
| } |
| |
| /// Tokenize source after it has been normalized. |
| fn tokenize_normalized( |
| grammar: &Grammar, |
| coverage: &mut Coverage, |
| src: &str, |
| ) -> Result<Tokens, ParseError> { |
| let top_prods = get_top_prods(grammar); |
| |
| let mut index = SourceIndex(0); |
| // Remove BOM |
| if src.starts_with('\u{FEFF}') { |
| index.0 += 3; |
| } |
| |
| let shebang; |
| (shebang, index) = parse_shebang(grammar, coverage, src, index)?; |
| let frontmatter; |
| (frontmatter, index) = parse_frontmatter(grammar, coverage, src, index)?; |
| let tokens = parse_tokens(grammar, coverage, &top_prods, src, index)?; |
| validate_delimiters_balanced(&tokens, src)?; |
| |
| debug!("lexing complete"); |
| |
| let tokens = Tokens { |
| tokens, |
| shebang, |
| frontmatter, |
| }; |
| |
| Ok(tokens) |
| } |
| |
| /// Returns the [`Production`]s that correspond to top-level tokens. |
| fn get_top_prods(grammar: &Grammar) -> Vec<&Production> { |
| let mut top_prods = Vec::new(); |
| let mut collect = |name| { |
| let prod = grammar.productions.get(name).unwrap(); |
| let ExpressionKind::Alt(es) = &prod.expression.kind else { |
| panic!("expected alts"); |
| }; |
| for e in es { |
| let nt = match &e.kind { |
| ExpressionKind::Sequence(es) => { |
| let seq: Vec<_> = es |
| .iter() |
| .filter_map(|e| match &e.kind { |
| ExpressionKind::Nt(nt) => Some(nt), |
| ExpressionKind::Break(_) | ExpressionKind::Comment(_) => None, |
| kind => panic!("unexpected kind {kind:?}"), |
| }) |
| .collect(); |
| assert_eq!(seq.len(), 1); |
| seq[0] |
| } |
| ExpressionKind::Nt(nt) => nt, |
| kind => panic!("unexpected kind {kind:?}"), |
| }; |
| top_prods.push(grammar.productions.get(nt).unwrap()); |
| } |
| }; |
| collect("COMMENT"); |
| collect("Token"); |
| top_prods |
| } |
| |
| fn parse_shebang( |
| grammar: &Grammar, |
| coverage: &mut Coverage, |
| src: &str, |
| index: SourceIndex, |
| ) -> Result<(Option<Node>, SourceIndex), ParseError> { |
| let shebang = grammar.productions.get("SHEBANG").unwrap(); |
| if let Some((node, next_index)) = parse_production(grammar, coverage, shebang, &src, index)? { |
| Ok((Some(node), next_index)) |
| } else { |
| Ok((None, index)) |
| } |
| } |
| |
| fn parse_frontmatter( |
| grammar: &Grammar, |
| coverage: &mut Coverage, |
| src: &str, |
| index: SourceIndex, |
| ) -> Result<(Option<Node>, SourceIndex), ParseError> { |
| let frontmatter = grammar.productions.get("FRONTMATTER").unwrap(); |
| if let Some((node, next_index)) = parse_production(grammar, coverage, frontmatter, &src, index) |
| .map_err(|e| ParseError { |
| message: format!("invalid frontmatter: {}", e.message), |
| byte_offset: e.byte_offset, |
| })? |
| { |
| Ok((Some(node), next_index)) |
| } else { |
| Ok((None, index)) |
| } |
| } |
| |
| /// Performs the actual parsing of all the tokens in the source. |
| fn parse_tokens( |
| grammar: &Grammar, |
| coverage: &mut Coverage, |
| top_prods: &[&Production], |
| src: &str, |
| mut index: SourceIndex, |
| ) -> Result<Vec<Node>, ParseError> { |
| let mut tokens = Vec::new(); |
| let whitespace = grammar.productions.get("WHITESPACE").unwrap(); |
| |
| while index.0 < src.len() { |
| if let Some((_node, next_index)) = |
| parse_production(grammar, coverage, whitespace, &src, index)? |
| { |
| index = next_index; |
| continue; |
| } |
| |
| let mut matched_token = None; |
| for token_prod in top_prods { |
| debug!("try top-level token `{}`", token_prod.name); |
| if let Some((node, next_index)) = |
| parse_production(grammar, coverage, token_prod, &src, index)? |
| && node.byte_len() > 0 |
| { |
| index = next_index; |
| matched_token = Some(node); |
| break; |
| } |
| } |
| |
| match matched_token { |
| Some(mut node) => { |
| normalize_line_doc(&mut node, src); |
| tokens.push(node); |
| } |
| None => { |
| return Err(ParseError { |
| byte_offset: index.0, |
| message: String::from("no tokens matched"), |
| }); |
| } |
| } |
| } |
| Ok(tokens) |
| } |
| |
| fn validate_delimiters_balanced(tokens: &[Node], src: &str) -> Result<(), ParseError> { |
| let mut stack = Vec::new(); |
| for token in tokens { |
| let text = &src[token.range.clone()]; |
| match text { |
| "(" | "[" | "{" => stack.push((text, token.range.start)), |
| ")" => { |
| if stack.pop().map(|(s, _)| s) != Some("(") { |
| return Err(ParseError { |
| byte_offset: token.range.start, |
| message: "unbalanced `)`".to_string(), |
| }); |
| } |
| } |
| "]" => { |
| if stack.pop().map(|(s, _)| s) != Some("[") { |
| return Err(ParseError { |
| byte_offset: token.range.start, |
| message: "unbalanced `]`".to_string(), |
| }); |
| } |
| } |
| "}" => { |
| if stack.pop().map(|(s, _)| s) != Some("{") { |
| return Err(ParseError { |
| byte_offset: token.range.start, |
| message: "unbalanced `}`".to_string(), |
| }); |
| } |
| } |
| _ => {} |
| } |
| } |
| if let Some((_, offset)) = stack.pop() { |
| return Err(ParseError { |
| byte_offset: offset, |
| message: "unclosed delimiter".to_string(), |
| }); |
| } |
| Ok(()) |
| } |
| |
| /// Fix line doc comment range. |
| /// |
| /// The Reference models line doc comments as *content* followed by a |
| /// linefeed. However, rustc and proc-macro2 model it as everything excluding |
| /// the linefeed. For convenience, this normalizes the range so that it |
| /// matches the other tools. |
| /// |
| /// A real implementation using the Reference lexer would extract the content |
| /// from `LINE_DOC_COMMENT_CONTENT`, which does not include the linefeed. |
| fn normalize_line_doc(node: &mut Node, src: &str) { |
| if matches!(node.name.as_str(), "INNER_LINE_DOC" | "OUTER_LINE_DOC") |
| && src[node.range.clone()].ends_with('\n') |
| { |
| node.range.end -= 1; |
| } |
| } |