use crate::span::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenKind {
FieldName,
Colon,
SectionHeader,
SectionArg,
If,
Else,
Elif,
Value,
Comma,
LParen,
RParen,
Not,
And,
Or,
CompOp,
Comment,
Eof,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriviaKind {
Whitespace,
Newline,
Comment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TriviaPiece {
pub kind: TriviaKind,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
pub indent: usize,
pub leading_trivia: Vec<TriviaPiece>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum LineKind {
Blank,
Comment,
SectionHeader,
Conditional,
Field,
Value,
}
#[derive(Debug, Clone)]
struct RawLine {
start: usize,
end: usize,
newline_start: usize,
line_end_with_newline: usize,
indent: Option<usize>,
content_start: usize,
kind: LineKind,
}
const SECTION_KEYWORDS: &[&str] = &[
"library",
"executable",
"test-suite",
"benchmark",
"flag",
"source-repository",
"common",
"custom-setup",
"foreign-library",
];
const CONDITIONAL_KEYWORDS: &[&str] = &["if", "else", "elif"];
fn is_section_keyword(word: &str) -> bool {
SECTION_KEYWORDS
.iter()
.any(|kw| kw.eq_ignore_ascii_case(word))
}
fn is_conditional_keyword(word: &str) -> bool {
CONDITIONAL_KEYWORDS
.iter()
.any(|kw| kw.eq_ignore_ascii_case(word))
}
fn visual_column(source: &[u8], start: usize, end: usize) -> usize {
let mut col: usize = 0;
for &b in &source[start..end] {
if b == b'\t' {
col = (col + 8) & !7; } else {
col += 1;
}
}
col
}
fn scan_word(source: &[u8], pos: usize) -> (usize, usize) {
let start = pos;
let mut i = pos;
while i < source.len()
&& (source[i].is_ascii_alphanumeric() || source[i] == b'-' || source[i] == b'_')
{
i += 1;
}
(start, i)
}
fn skip_hspace(source: &[u8], pos: usize) -> usize {
let mut i = pos;
while i < source.len() && (source[i] == b' ' || source[i] == b'\t') {
i += 1;
}
i
}
fn split_lines(source: &str) -> Vec<RawLine> {
let bytes = source.as_bytes();
let len = bytes.len();
let mut lines = Vec::new();
let mut pos = 0;
while pos <= len {
let line_start = pos;
let mut end = pos;
while end < len && bytes[end] != b'\n' && bytes[end] != b'\r' {
end += 1;
}
let content_end = end;
let newline_start = end;
if end < len && bytes[end] == b'\r' {
end += 1;
}
if end < len && bytes[end] == b'\n' {
end += 1;
}
let line_end = end;
let mut first_non_ws = line_start;
while first_non_ws < content_end
&& (bytes[first_non_ws] == b' ' || bytes[first_non_ws] == b'\t')
{
first_non_ws += 1;
}
let indent = if first_non_ws == content_end {
None } else {
Some(visual_column(bytes, line_start, first_non_ws))
};
let kind = classify_line(source, first_non_ws, content_end, indent.is_none());
lines.push(RawLine {
start: line_start,
end: content_end,
newline_start,
line_end_with_newline: line_end,
indent,
content_start: first_non_ws,
kind,
});
if line_end == pos {
break;
}
pos = line_end;
}
reclassify_braced_freeform_blocks(&mut lines, source);
lines
}
fn reclassify_braced_freeform_blocks(lines: &mut [RawLine], source: &str) {
let bytes = source.as_bytes();
let mut i = 0;
while i < lines.len() {
if lines[i].kind == LineKind::Field {
let line = &lines[i];
let mut check = line.end;
while check > line.content_start
&& (bytes[check - 1] == b' ' || bytes[check - 1] == b'\t')
{
check -= 1;
}
if check > line.content_start && bytes[check - 1] == b'{' {
i += 1;
while i < lines.len() {
let inner = &lines[i];
let trimmed_start = inner.content_start;
let trimmed_end = inner.end;
if trimmed_start < trimmed_end
&& bytes[trimmed_start] == b'}'
&& is_only_closing_brace(bytes, trimmed_start, trimmed_end)
{
lines[i].kind = LineKind::Value;
i += 1;
break;
}
if inner.kind != LineKind::Blank {
lines[i].kind = LineKind::Value;
}
i += 1;
}
continue;
}
}
i += 1;
}
}
fn is_only_closing_brace(bytes: &[u8], start: usize, end: usize) -> bool {
if start >= end || bytes[start] != b'}' {
return false;
}
for &b in &bytes[start + 1..end] {
if b != b' ' && b != b'\t' {
return false;
}
}
true
}
fn classify_line(
source: &str,
content_start: usize,
content_end: usize,
is_blank: bool,
) -> LineKind {
if is_blank {
return LineKind::Blank;
}
let bytes = source.as_bytes();
if content_start + 1 < content_end
&& bytes[content_start] == b'-'
&& bytes[content_start + 1] == b'-'
{
return LineKind::Comment;
}
let (word_start, word_end) = scan_word(bytes, content_start);
if word_start == word_end {
return LineKind::Value;
}
let word = &source[word_start..word_end];
if is_section_keyword(word) {
if word_end >= content_end {
return LineKind::SectionHeader;
}
let ch = bytes[word_end];
if ch == b' ' || ch == b'\t' || ch == b'{' {
return LineKind::SectionHeader;
}
}
if is_conditional_keyword(word) {
let after_word = skip_hspace(bytes, word_end);
if after_word >= content_end || bytes[after_word] != b':' {
return LineKind::Conditional;
}
}
let after_word = skip_hspace(bytes, word_end);
if after_word < content_end && bytes[after_word] == b':' {
return LineKind::Field;
}
LineKind::Value
}
pub fn tokenize(source: &str) -> Vec<Token> {
let lines = split_lines(source);
let mut tokens = Vec::new();
let mut pending_trivia: Vec<TriviaPiece> = Vec::new();
for line in &lines {
match line.kind {
LineKind::Blank => {
if line.start < line.end {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(line.start, line.end),
});
}
if line.newline_start < line.line_end_with_newline {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Newline,
span: Span::new(line.newline_start, line.line_end_with_newline),
});
}
}
LineKind::Comment => {
if line.start < line.content_start {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(line.start, line.content_start),
});
}
let comment_span = Span::new(line.content_start, line.end);
let trivia = std::mem::take(&mut pending_trivia);
tokens.push(Token {
kind: TokenKind::Comment,
span: comment_span,
indent: line.indent.unwrap_or(0),
leading_trivia: trivia,
});
if line.newline_start < line.line_end_with_newline {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Newline,
span: Span::new(line.newline_start, line.line_end_with_newline),
});
}
}
LineKind::SectionHeader => {
tokenize_section_header(source, line, &mut tokens, &mut pending_trivia);
}
LineKind::Conditional => {
tokenize_conditional(source, line, &mut tokens, &mut pending_trivia);
}
LineKind::Field => {
tokenize_field(source, line, &mut tokens, &mut pending_trivia);
}
LineKind::Value => {
tokenize_value_line(source, line, &mut tokens, &mut pending_trivia);
}
}
}
let eof_offset = source.len();
tokens.push(Token {
kind: TokenKind::Eof,
span: Span::empty(eof_offset),
indent: 0,
leading_trivia: std::mem::take(&mut pending_trivia),
});
tokens
}
fn tokenize_section_header(
source: &str,
line: &RawLine,
tokens: &mut Vec<Token>,
pending_trivia: &mut Vec<TriviaPiece>,
) {
let bytes = source.as_bytes();
if line.start < line.content_start {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(line.start, line.content_start),
});
}
let (kw_start, kw_end) = scan_word(bytes, line.content_start);
tokens.push(Token {
kind: TokenKind::SectionHeader,
span: Span::new(kw_start, kw_end),
indent: line.indent.unwrap_or(0),
leading_trivia: std::mem::take(pending_trivia),
});
let mut pos = kw_end;
let ws_start = pos;
pos = skip_hspace(bytes, pos);
if ws_start < pos {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(ws_start, pos),
});
}
if pos < line.end {
let mut arg_end = line.end;
while arg_end > pos && (bytes[arg_end - 1] == b' ' || bytes[arg_end - 1] == b'\t') {
arg_end -= 1;
}
if pos < arg_end {
tokens.push(Token {
kind: TokenKind::SectionArg,
span: Span::new(pos, arg_end),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
if arg_end < line.end {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(arg_end, line.end),
});
}
}
}
if line.newline_start < line.line_end_with_newline {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Newline,
span: Span::new(line.newline_start, line.line_end_with_newline),
});
}
}
fn tokenize_conditional(
source: &str,
line: &RawLine,
tokens: &mut Vec<Token>,
pending_trivia: &mut Vec<TriviaPiece>,
) {
let bytes = source.as_bytes();
if line.start < line.content_start {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(line.start, line.content_start),
});
}
let (kw_start, kw_end) = scan_word(bytes, line.content_start);
let kw_str = &source[kw_start..kw_end];
let kind = if kw_str.eq_ignore_ascii_case("if") {
TokenKind::If
} else if kw_str.eq_ignore_ascii_case("else") {
TokenKind::Else
} else {
TokenKind::Elif
};
tokens.push(Token {
kind,
span: Span::new(kw_start, kw_end),
indent: line.indent.unwrap_or(0),
leading_trivia: std::mem::take(pending_trivia),
});
if kind == TokenKind::If || kind == TokenKind::Elif {
tokenize_condition_expr(source, bytes, kw_end, line, tokens, pending_trivia);
} else if kind == TokenKind::Else {
let after_kw = skip_hspace(bytes, kw_end);
if after_kw < line.end {
if kw_end < after_kw {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(kw_end, after_kw),
});
}
tokens.push(Token {
kind: TokenKind::Value,
span: Span::new(after_kw, line.end),
indent: visual_column(bytes, line.start, after_kw),
leading_trivia: std::mem::take(pending_trivia),
});
}
}
if line.newline_start < line.line_end_with_newline {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Newline,
span: Span::new(line.newline_start, line.line_end_with_newline),
});
}
}
fn tokenize_condition_expr(
_source: &str,
bytes: &[u8],
start: usize,
line: &RawLine,
tokens: &mut Vec<Token>,
pending_trivia: &mut Vec<TriviaPiece>,
) {
let end = line.end;
let mut pos = start;
while pos < end {
let b = bytes[pos];
match b {
b' ' | b'\t' => {
let ws_start = pos;
pos = skip_hspace(bytes, pos);
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(ws_start, pos),
});
}
b'(' => {
tokens.push(Token {
kind: TokenKind::LParen,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
b')' => {
tokens.push(Token {
kind: TokenKind::RParen,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
b'!' => {
tokens.push(Token {
kind: TokenKind::Not,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
b'&' => {
if pos + 1 < end && bytes[pos + 1] == b'&' {
tokens.push(Token {
kind: TokenKind::And,
span: Span::new(pos, pos + 2),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 2;
} else {
tokens.push(Token {
kind: TokenKind::Value,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
}
b'|' => {
if pos + 1 < end && bytes[pos + 1] == b'|' {
tokens.push(Token {
kind: TokenKind::Or,
span: Span::new(pos, pos + 2),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 2;
} else {
tokens.push(Token {
kind: TokenKind::Value,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
}
b'>' if pos + 1 < end && bytes[pos + 1] == b'=' => {
tokens.push(Token {
kind: TokenKind::CompOp,
span: Span::new(pos, pos + 2),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 2;
}
b'<' if pos + 1 < end && bytes[pos + 1] == b'=' => {
tokens.push(Token {
kind: TokenKind::CompOp,
span: Span::new(pos, pos + 2),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 2;
}
b'=' => {
let len = if pos + 1 < end && bytes[pos + 1] == b'=' {
2
} else {
1
};
tokens.push(Token {
kind: TokenKind::CompOp,
span: Span::new(pos, pos + len),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += len;
}
b'>' => {
tokens.push(Token {
kind: TokenKind::CompOp,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
b'<' => {
tokens.push(Token {
kind: TokenKind::CompOp,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
b',' => {
tokens.push(Token {
kind: TokenKind::Comma,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
b'-' if pos + 1 < end && bytes[pos + 1] == b'-' => {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Comment,
span: Span::new(pos, end),
});
pos = end;
}
_ => {
let val_start = pos;
pos += 1;
while pos < end
&& !matches!(
bytes[pos],
b' ' | b'\t' | b'(' | b')' | b'!' | b',' | b'&' | b'|' | b'>' | b'<' | b'='
)
{
pos += 1;
}
tokens.push(Token {
kind: TokenKind::Value,
span: Span::new(val_start, pos),
indent: visual_column(bytes, line.start, val_start),
leading_trivia: std::mem::take(pending_trivia),
});
}
}
}
}
fn tokenize_field(
source: &str,
line: &RawLine,
tokens: &mut Vec<Token>,
pending_trivia: &mut Vec<TriviaPiece>,
) {
let bytes = source.as_bytes();
if line.start < line.content_start {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(line.start, line.content_start),
});
}
let (name_start, name_end) = scan_word(bytes, line.content_start);
tokens.push(Token {
kind: TokenKind::FieldName,
span: Span::new(name_start, name_end),
indent: line.indent.unwrap_or(0),
leading_trivia: std::mem::take(pending_trivia),
});
let mut pos = name_end;
let ws_start = pos;
pos = skip_hspace(bytes, pos);
if ws_start < pos {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(ws_start, pos),
});
}
if pos < line.end && bytes[pos] == b':' {
tokens.push(Token {
kind: TokenKind::Colon,
span: Span::new(pos, pos + 1),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
pos += 1;
}
let ws_start2 = pos;
pos = skip_hspace(bytes, pos);
if ws_start2 < pos {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(ws_start2, pos),
});
}
if pos < line.end {
let val_end = line.end;
tokens.push(Token {
kind: TokenKind::Value,
span: Span::new(pos, val_end),
indent: visual_column(bytes, line.start, pos),
leading_trivia: std::mem::take(pending_trivia),
});
}
if line.newline_start < line.line_end_with_newline {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Newline,
span: Span::new(line.newline_start, line.line_end_with_newline),
});
}
}
fn tokenize_value_line(
source: &str,
line: &RawLine,
tokens: &mut Vec<Token>,
pending_trivia: &mut Vec<TriviaPiece>,
) {
let _ = source;
if line.start < line.content_start {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Whitespace,
span: Span::new(line.start, line.content_start),
});
}
if line.content_start < line.end {
tokens.push(Token {
kind: TokenKind::Value,
span: Span::new(line.content_start, line.end),
indent: line.indent.unwrap_or(0),
leading_trivia: std::mem::take(pending_trivia),
});
}
if line.newline_start < line.line_end_with_newline {
pending_trivia.push(TriviaPiece {
kind: TriviaKind::Newline,
span: Span::new(line.newline_start, line.line_end_with_newline),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tok_pairs(source: &str) -> Vec<(TokenKind, &str)> {
let tokens = tokenize(source);
tokens
.iter()
.map(|t| (t.kind, t.span.slice(source)))
.collect()
}
#[test]
fn lex_simple_field() {
let src = "name: foo\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::FieldName, "name"),
(TokenKind::Colon, ":"),
(TokenKind::Value, "foo"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_field_with_spaces() {
let src = "build-depends: base >=4.14\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::FieldName, "build-depends"),
(TokenKind::Colon, ":"),
(TokenKind::Value, "base >=4.14"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_section_header_no_arg() {
let src = "library\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![(TokenKind::SectionHeader, "library"), (TokenKind::Eof, ""),]
);
}
#[test]
fn lex_section_header_with_arg() {
let src = "executable my-exe\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::SectionHeader, "executable"),
(TokenKind::SectionArg, "my-exe"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_conditional_if() {
let src = " if flag(dev)\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::If, "if"),
(TokenKind::Value, "flag"),
(TokenKind::LParen, "("),
(TokenKind::Value, "dev"),
(TokenKind::RParen, ")"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_conditional_complex() {
let src = " if flag(dev) && !os(windows)\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::If, "if"),
(TokenKind::Value, "flag"),
(TokenKind::LParen, "("),
(TokenKind::Value, "dev"),
(TokenKind::RParen, ")"),
(TokenKind::And, "&&"),
(TokenKind::Not, "!"),
(TokenKind::Value, "os"),
(TokenKind::LParen, "("),
(TokenKind::Value, "windows"),
(TokenKind::RParen, ")"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_else() {
let src = " else\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![(TokenKind::Else, "else"), (TokenKind::Eof, ""),]
);
}
#[test]
fn lex_comment_line() {
let src = "-- this is a comment\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::Comment, "-- this is a comment"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_blank_lines() {
let src = "name: foo\n\nversion: 0.1\n";
let tokens = tokenize(src);
let version_tok = tokens
.iter()
.find(|t| t.kind == TokenKind::FieldName && t.span.slice(src) == "version");
assert!(version_tok.is_some());
let trivia_kinds: Vec<_> = version_tok
.unwrap()
.leading_trivia
.iter()
.map(|t| t.kind)
.collect();
assert!(trivia_kinds.contains(&TriviaKind::Newline));
}
#[test]
fn lex_indented_field() {
let src = " exposed-modules: Foo\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::FieldName, "exposed-modules"),
(TokenKind::Colon, ":"),
(TokenKind::Value, "Foo"),
(TokenKind::Eof, ""),
]
);
let tokens = tokenize(src);
assert_eq!(tokens[0].indent, 2);
}
#[test]
fn lex_continuation_value() {
let src = " base >=4.14\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![(TokenKind::Value, "base >=4.14"), (TokenKind::Eof, ""),]
);
let tokens = tokenize(src);
assert_eq!(tokens[0].indent, 4);
}
#[test]
fn lex_full_span_coverage() {
let src = "name: foo\nversion: 0.1\n";
let tokens = tokenize(src);
let mut covered = vec![false; src.len()];
for tok in &tokens {
for tp in &tok.leading_trivia {
for (i, is_covered) in covered
.iter_mut()
.enumerate()
.take(tp.span.end)
.skip(tp.span.start)
{
assert!(
!*is_covered,
"byte {i} covered twice (trivia on {:?})",
tok.kind
);
*is_covered = true;
}
}
for (i, is_covered) in covered
.iter_mut()
.enumerate()
.take(tok.span.end)
.skip(tok.span.start)
{
assert!(
!*is_covered,
"byte {i} covered twice (token {:?})",
tok.kind
);
*is_covered = true;
}
}
for (i, &c) in covered.iter().enumerate() {
assert!(c, "byte {i} ({:?}) not covered", src.as_bytes()[i] as char);
}
}
#[test]
fn lex_impl_condition() {
let src = " if impl(ghc >= 9.6)\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::If, "if"),
(TokenKind::Value, "impl"),
(TokenKind::LParen, "("),
(TokenKind::Value, "ghc"),
(TokenKind::CompOp, ">="),
(TokenKind::Value, "9.6"),
(TokenKind::RParen, ")"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_field_no_value() {
let src = "build-depends:\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::FieldName, "build-depends"),
(TokenKind::Colon, ":"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_import_as_field() {
let src = " import: warnings\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::FieldName, "import"),
(TokenKind::Colon, ":"),
(TokenKind::Value, "warnings"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_tab_indent() {
let src = "\texposed-modules: Foo\n";
let tokens = tokenize(src);
assert_eq!(tokens[0].indent, 8);
}
#[test]
fn lex_no_trailing_newline() {
let src = "name: foo";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::FieldName, "name"),
(TokenKind::Colon, ":"),
(TokenKind::Value, "foo"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn lex_common_stanza() {
let src = "common warnings\n";
let pairs = tok_pairs(src);
assert_eq!(
pairs,
vec![
(TokenKind::SectionHeader, "common"),
(TokenKind::SectionArg, "warnings"),
(TokenKind::Eof, ""),
]
);
}
#[test]
fn full_span_coverage_multiline() {
let src = "cabal-version: 3.0\nname: foo\n\n-- A comment\n\nlibrary\n exposed-modules: Foo\n build-depends:\n base >=4.14\n";
let tokens = tokenize(src);
let mut covered = vec![false; src.len()];
for tok in &tokens {
for tp in &tok.leading_trivia {
for (i, is_covered) in covered
.iter_mut()
.enumerate()
.take(tp.span.end)
.skip(tp.span.start)
{
assert!(!*is_covered, "byte {i} covered twice (trivia)");
*is_covered = true;
}
}
for (i, is_covered) in covered
.iter_mut()
.enumerate()
.take(tok.span.end)
.skip(tok.span.start)
{
assert!(
!*is_covered,
"byte {i} covered twice (token {:?})",
tok.kind
);
*is_covered = true;
}
}
for (i, &c) in covered.iter().enumerate() {
assert!(c, "byte {i} ({:?}) not covered", src.as_bytes()[i] as char);
}
}
}