Improve block body's diagnostics
This commit is contained in:
parent
9d3313995e
commit
b1f0714483
@ -16,45 +16,40 @@ use rottlib::parser::Parser;
|
||||
/// Keep these small: the goal is to inspect lexer diagnostics and delimiter
|
||||
/// recovery behavior, not full parser behavior.
|
||||
const TEST_CASES: &[(&str, &str)] = &[
|
||||
// L0001: invalid or unknown token
|
||||
(
|
||||
"files/L0001_01.uc",
|
||||
"`",
|
||||
),
|
||||
|
||||
// L0002: unexpected closing delimiter
|
||||
(
|
||||
"files/L0002_01.uc",
|
||||
"]",
|
||||
),
|
||||
|
||||
// L0003: unclosed delimiter before later closing delimiter
|
||||
// P0027: `else` without a matching `if`
|
||||
//
|
||||
// The `}` can still recover by matching the earlier `{`.
|
||||
// `else` cannot start a standalone block item. The parser should report
|
||||
// P0027 at `else`, then recover by bailing out of the current block.
|
||||
(
|
||||
"files/L0003_01.uc",
|
||||
"{\n foo(\n}\n",
|
||||
"files/P0027_01.uc",
|
||||
"{\n local bool bReady;\n bReady = CheckReady();\n else { StartMatch(); }\n NotifyReady();\n}\n",
|
||||
),
|
||||
|
||||
// L0004: mismatched closing delimiter
|
||||
(
|
||||
"files/L0004_01.uc",
|
||||
"(]",
|
||||
),
|
||||
|
||||
// L0005: unclosed delimiter at end of file
|
||||
(
|
||||
"files/L0005_01.uc",
|
||||
"foo(",
|
||||
),
|
||||
|
||||
// Mixed recovery case:
|
||||
// P0027: `case` outside of a `switch`
|
||||
//
|
||||
// `)` recovers by matching `(` after treating `[` as unclosed;
|
||||
// the following `]` is then unexpected.
|
||||
// `case` is a switch-arm boundary, not a valid statement or expression
|
||||
// starter in an ordinary block.
|
||||
(
|
||||
"files/L_mixed_01.uc",
|
||||
"([)]",
|
||||
"files/P0027_02.uc",
|
||||
"{ local int Count; Count = 3; case 3: Count++; UpdateHud();}",
|
||||
),
|
||||
|
||||
// P0027: standalone `until` without a preceding `do`
|
||||
//
|
||||
// `until` is only meaningful as the tail of `do ... until`, so it should
|
||||
// not be accepted as a normal block item.
|
||||
(
|
||||
"files/P0027_03.uc",
|
||||
"{\n local bool bDone;\n bDone = false;\n until (bDone)\n TickWork();\n}\n",
|
||||
),
|
||||
|
||||
// P0027: preprocessor/exec directive inside a statement block
|
||||
//
|
||||
// `#exec` is declaration/top-level-like syntax, not a valid statement or
|
||||
// expression inside a braced statement block.
|
||||
(
|
||||
"files/P0027_04.uc",
|
||||
"{\n local int Count;\n Count = 0;\n #exec TEXTURE IMPORT NAME=Bad FILE=Bad.bmp\n Count++;\n}\n",
|
||||
),
|
||||
];
|
||||
|
||||
@ -62,12 +57,12 @@ const TEST_CASES: &[(&str, &str)] = &[
|
||||
///
|
||||
/// For lexer-focused fixtures this is usually noisy, so keep it off unless you
|
||||
/// want to inspect how parser recovery behaves after lexer diagnostics.
|
||||
const RUN_PARSER: bool = false;
|
||||
const RUN_PARSER: bool = true;
|
||||
|
||||
/// If true, print the parsed expression using Debug formatting.
|
||||
const PRINT_PARSED_EXPR: bool = false;
|
||||
|
||||
/// If true, print diagnostics even when parsing returned a value.
|
||||
const PRINT_LEXER_DIAGNOSTICS: bool = false;
|
||||
const ALWAYS_PRINT_DIAGNOSTICS: bool = true;
|
||||
|
||||
fn main() {
|
||||
@ -89,7 +84,7 @@ fn main() {
|
||||
} else {
|
||||
had_any_problem = true;
|
||||
|
||||
if ALWAYS_PRINT_DIAGNOSTICS {
|
||||
if PRINT_LEXER_DIAGNOSTICS {
|
||||
println!("Lexer diagnostics:");
|
||||
for diag in lexer_diagnostics {
|
||||
render_diagnostic(diag, &tf, Some(label), false);
|
||||
|
||||
111
rottlib/src/diagnostics/parse_error_diagnostics/block_items.rs
Normal file
111
rottlib/src/diagnostics/parse_error_diagnostics/block_items.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use super::{Diagnostic, DiagnosticBuilder, FoundAt, found_at};
|
||||
use crate::lexer::{Token, TokenSpan, TokenizedFile};
|
||||
use crate::parser::ParseError;
|
||||
|
||||
/// P0025
|
||||
pub(super) fn diagnostic_block_missing_semicolon_after_expression<'src>(
|
||||
error: ParseError,
|
||||
file: &TokenizedFile<'src>,
|
||||
) -> Diagnostic {
|
||||
let expression_span = error.related_spans.get("expression_span").copied();
|
||||
|
||||
let primary_span = TokenSpan::new(error.blame_span.end);
|
||||
|
||||
let primary_text = match found_at(file, primary_span.end) {
|
||||
FoundAt::Token(token_text) => format!("expected `;` before `{}`", token_text),
|
||||
FoundAt::EndOfFile => "expected `;` before end of file".to_string(),
|
||||
FoundAt::Unknown => "expected `;` here".to_string(),
|
||||
};
|
||||
|
||||
let mut builder = DiagnosticBuilder::error("missing `;` after expression statement");
|
||||
|
||||
if let Some(expression_span) = expression_span {
|
||||
if file.same_line(expression_span.start, primary_span.end) {
|
||||
builder = builder.secondary_label(expression_span, "expression statement");
|
||||
} else {
|
||||
builder = builder.secondary_label(
|
||||
TokenSpan::new(expression_span.end),
|
||||
"expression statement ends here",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
.primary_label(primary_span, primary_text)
|
||||
.code("P0025")
|
||||
.build()
|
||||
}
|
||||
|
||||
/// P0026
|
||||
pub(super) fn diagnostic_block_missing_closing_brace<'src>(
|
||||
error: ParseError,
|
||||
file: &TokenizedFile<'src>,
|
||||
) -> Diagnostic {
|
||||
let left_brace_span = error.related_spans.get("left_brace").copied();
|
||||
let unexpected_token_span = error.related_spans.get("unexpected_token").copied();
|
||||
|
||||
let (mut primary_span, primary_text) =
|
||||
if let Some(unexpected_token_span) = unexpected_token_span {
|
||||
let primary_span = TokenSpan::new(unexpected_token_span.end);
|
||||
|
||||
let primary_text = match found_at(file, primary_span.end) {
|
||||
FoundAt::Token(token_text) => format!("expected `}}` before `{}`", token_text),
|
||||
FoundAt::EndOfFile => "expected `}` before end of file".to_string(),
|
||||
FoundAt::Unknown => "expected `}` here".to_string(),
|
||||
};
|
||||
|
||||
(primary_span, primary_text)
|
||||
} else {
|
||||
(
|
||||
TokenSpan::new(error.blame_span.end),
|
||||
"expected `}` before end of file".to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let builder = DiagnosticBuilder::error("missing `}` to close block");
|
||||
|
||||
if let Some(left_brace_span) = left_brace_span
|
||||
&& !file.same_line(left_brace_span.start, primary_span.end)
|
||||
{
|
||||
primary_span.start = left_brace_span.start;
|
||||
}
|
||||
|
||||
builder
|
||||
.primary_label(primary_span, primary_text)
|
||||
.code("P0026")
|
||||
.build()
|
||||
}
|
||||
|
||||
/// P0027
|
||||
pub(super) fn diagnostic_block_expected_item<'src>(
|
||||
error: ParseError,
|
||||
file: &TokenizedFile<'src>,
|
||||
) -> Diagnostic {
|
||||
let primary_span = error.blame_span;
|
||||
|
||||
let (title, primary_text) = match file.token_at(primary_span.start).map(|data| data.token) {
|
||||
Some(Token::ExecDirective) => (
|
||||
"expected statement or expression, found `#exec` directive".to_string(),
|
||||
"`#exec` directives are not allowed in a statement block".to_string(),
|
||||
),
|
||||
_ => match found_at(file, primary_span.start) {
|
||||
FoundAt::Token(token_text) => (
|
||||
format!("expected statement or expression, found `{}`", token_text),
|
||||
format!("unexpected `{}`", token_text),
|
||||
),
|
||||
FoundAt::EndOfFile => (
|
||||
"expected statement or expression, found end of file".to_string(),
|
||||
"reached end of file here".to_string(),
|
||||
),
|
||||
FoundAt::Unknown => (
|
||||
"expected statement or expression".to_string(),
|
||||
"expected statement or expression here".to_string(),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
DiagnosticBuilder::error(title)
|
||||
.primary_label(primary_span, primary_text)
|
||||
.code("P0027")
|
||||
.build()
|
||||
}
|
||||
@ -263,7 +263,7 @@ pub(super) fn diagnostic_for_each_iterator_expression_expected<'src>(
|
||||
let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end));
|
||||
|
||||
let found = found_at(file, error.blame_span.end);
|
||||
let found_body_block_start = matches!(found, FoundAt::Token("{"));
|
||||
let _ = matches!(found, FoundAt::Token("{"));
|
||||
|
||||
let (header_text, primary_text) = match (control_keyword_text, found) {
|
||||
(Some(keyword_text), FoundAt::Token(token_text)) => {
|
||||
|
||||
@ -10,9 +10,14 @@
|
||||
//! parser areas or grammar families.
|
||||
|
||||
use super::{Diagnostic, DiagnosticBuilder};
|
||||
use crate::diagnostics::parse_error_diagnostics::block_items::{
|
||||
diagnostic_block_expected_item, diagnostic_block_missing_closing_brace,
|
||||
diagnostic_block_missing_semicolon_after_expression,
|
||||
};
|
||||
use crate::lexer::{TokenPosition, TokenSpan, TokenizedFile};
|
||||
use crate::parser::{ParseError, ParseErrorKind};
|
||||
|
||||
mod block_items;
|
||||
mod control_flow_expressions;
|
||||
mod primary_expressions;
|
||||
|
||||
@ -133,6 +138,14 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
|
||||
}
|
||||
ParseErrorKind::BreakValueInvalidStart => diagnostic_break_value_invalid_start(error, file),
|
||||
ParseErrorKind::GotoMissingLabel => diagnostic_goto_missing_label(error, file),
|
||||
// block_items.rs
|
||||
ParseErrorKind::BlockMissingSemicolonAfterExpression => {
|
||||
diagnostic_block_missing_semicolon_after_expression(error, file)
|
||||
}
|
||||
ParseErrorKind::BlockMissingClosingBrace => {
|
||||
diagnostic_block_missing_closing_brace(error, file)
|
||||
}
|
||||
ParseErrorKind::BlockExpectedItem => diagnostic_block_expected_item(error, file),
|
||||
|
||||
_ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind))
|
||||
.primary_label(error.covered_span, "happened here")
|
||||
|
||||
@ -59,7 +59,7 @@ pub enum BraceKind {
|
||||
#[logos(extras = LexerState)]
|
||||
pub enum RawToken {
|
||||
// # Compiler/directive keywords
|
||||
#[regex(r"(?i)#exec[^\r\n]*(?:\r\n|\n|\r)?")]
|
||||
#[regex(r"(?i)#exec[^\r\n]*")]
|
||||
ExecDirective,
|
||||
#[regex("(?i)cpptext", |lex| {
|
||||
if is_next_nontrivia_left_brace(lex) {
|
||||
|
||||
@ -364,6 +364,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
}
|
||||
|
||||
/// Ensures that parsing has advanced past `old_position`.
|
||||
// TODO: must be given peeked value!
|
||||
///
|
||||
/// This is intended as a safeguard against infinite-loop bugs while
|
||||
/// recovering from invalid input. In debug builds it asserts that progress
|
||||
|
||||
@ -64,6 +64,12 @@ pub enum ParseErrorKind {
|
||||
BreakValueInvalidStart,
|
||||
/// P0024
|
||||
GotoMissingLabel,
|
||||
/// P0025
|
||||
BlockMissingSemicolonAfterExpression,
|
||||
/// P0026
|
||||
BlockMissingClosingBrace,
|
||||
/// P0027
|
||||
BlockExpectedItem,
|
||||
// ================== Old errors to be thrown away! ==================
|
||||
/// Expression inside `(...)` could not be parsed and no closing `)`
|
||||
/// was found.
|
||||
@ -83,11 +89,7 @@ pub enum ParseErrorKind {
|
||||
|
||||
TypeSpecClassMissingInnerType,
|
||||
TypeSpecClassMissingClosingAngle,
|
||||
/// An expression inside a block is not terminated with `;`.
|
||||
BlockMissingSemicolonAfterExpression,
|
||||
/// A statement inside a block is not terminated with `;`.
|
||||
BlockMissingSemicolonAfterStatement,
|
||||
BlockMissingClosingBrace,
|
||||
/// `switch` has no body (missing matching braces).
|
||||
SwitchMissingBody,
|
||||
/// The first top-level item in a `switch` body is not a `case`.
|
||||
|
||||
@ -357,7 +357,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
Ok(rule) => rules.push(rule),
|
||||
Err(error) => {
|
||||
self.report_error(error);
|
||||
self.recover_until(SyncLevel::Statement);
|
||||
self.recover_until(SyncLevel::StatementStart);
|
||||
let _ = self.eat(Token::Semicolon);
|
||||
if !self.ensure_progress_or_break(loop_start) {
|
||||
break;
|
||||
@ -899,7 +899,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
}
|
||||
Some((_, _)) if declarators.is_empty() => {
|
||||
self.report_error_here(ParseErrorKind::DeclBadVariableIdentifier);
|
||||
self.recover_until(SyncLevel::Statement);
|
||||
self.recover_until(SyncLevel::StatementStart);
|
||||
let _ = self.eat(Token::Semicolon);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
variants: &mut ArenaVec<'arena, IdentifierToken>,
|
||||
) -> ControlFlow<()> {
|
||||
self.parse_identifier(ParseErrorKind::EnumBadVariant)
|
||||
.sync_error_until(self, SyncLevel::Statement)
|
||||
.sync_error_until(self, SyncLevel::StatementStart)
|
||||
.ok_or_report(self)
|
||||
.map_or(ControlFlow::Break(()), |variant| {
|
||||
variants.push(variant);
|
||||
@ -122,7 +122,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
let Some(variant) = self
|
||||
.parse_identifier(ParseErrorKind::EnumBadVariant)
|
||||
.widen_error_span_from(error_start_position)
|
||||
.sync_error_until(self, SyncLevel::Statement)
|
||||
.sync_error_until(self, SyncLevel::StatementStart)
|
||||
.ok_or_report(self)
|
||||
else {
|
||||
// If we don't even get a good identifier - error is different
|
||||
|
||||
@ -93,7 +93,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
self.advance();
|
||||
if !self.eat(Token::CppBlock) {
|
||||
self.report_error_here(ParseErrorKind::CppDirectiveMissingCppBlock);
|
||||
self.recover_until(SyncLevel::Statement);
|
||||
self.recover_until(SyncLevel::StatementStart);
|
||||
}
|
||||
StructBodyItemParseOutcome::Skip
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
) -> ControlFlow<()> {
|
||||
if let Some(parsed_declarator) = self
|
||||
.parse_variable_declarator()
|
||||
.sync_error_until(self, SyncLevel::Statement)
|
||||
.sync_error_until(self, SyncLevel::StatementStart)
|
||||
.ok_or_report(self)
|
||||
{
|
||||
declarators.push(parsed_declarator);
|
||||
@ -122,7 +122,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
if let Some(parsed_declarator) = self
|
||||
.parse_variable_declarator()
|
||||
.widen_error_span_from(error_start_position)
|
||||
.sync_error_until(self, SyncLevel::Statement)
|
||||
.sync_error_until(self, SyncLevel::StatementStart)
|
||||
.ok_or_report(self)
|
||||
{
|
||||
self.make_error_at_last_consumed(ParseErrorKind::DeclNoSeparatorBetweenVariableDeclarations)
|
||||
|
||||
@ -4,106 +4,187 @@
|
||||
//! function, loop, state, and similar constructs after the opening `{`
|
||||
//! has been consumed.
|
||||
|
||||
use crate::arena::ArenaVec;
|
||||
use crate::ast::{BlockBody, Expression, ExpressionRef, Statement, StatementRef};
|
||||
use crate::ast::{BlockBody, Expression, ExpressionRef, Statement, StatementList, StatementRef};
|
||||
use crate::lexer::{Token, TokenPosition, TokenSpan};
|
||||
use crate::parser::{ParseErrorKind, Parser};
|
||||
use crate::parser::{ParseErrorKind, ParseResult, Parser, ResultRecoveryExt, SyncLevel};
|
||||
|
||||
impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
/// Parses a `{ ... }` block after the opening `{` has been consumed.
|
||||
/// Parses a braced block body into an [`Expression::Block`].
|
||||
///
|
||||
/// Consumes tokens until the matching `}` and returns an
|
||||
/// [`Expression::Block`] whose span covers the entire block, from
|
||||
/// `opening_brace_position` to the closing `}`.
|
||||
/// The opening `{` must already have been consumed. The returned block's
|
||||
/// span covers the whole block, from `left_brace_position` through
|
||||
/// the closing `}`.
|
||||
///
|
||||
/// On premature end-of-file, returns a best-effort block.
|
||||
/// On premature end-of-file, reports the missing `}` and returns
|
||||
/// a best-effort block.
|
||||
#[must_use]
|
||||
pub(crate) fn parse_block_tail(
|
||||
pub(crate) fn parse_block_body_tail(
|
||||
&mut self,
|
||||
opening_brace_position: TokenPosition,
|
||||
left_brace_position: TokenPosition,
|
||||
) -> ExpressionRef<'src, 'arena> {
|
||||
let BlockBody { statements, span } =
|
||||
self.parse_braced_block_statements_tail(opening_brace_position);
|
||||
self.parse_braced_block_statements_tail(left_brace_position);
|
||||
self.arena.alloc_node(Expression::Block(statements), span)
|
||||
}
|
||||
|
||||
/// Parses a `{ ... }` block after the opening `{` has been consumed.
|
||||
/// Parses the statements in a braced block body.
|
||||
///
|
||||
/// Consumes tokens until the matching `}` and returns the contained
|
||||
/// statements together with a span that covers the entire block, from
|
||||
/// `opening_brace_position` to the closing `}`.
|
||||
/// The opening `{` must already have been consumed. Returns the parsed
|
||||
/// statements and a span covering the whole block, from
|
||||
/// `left_brace_position` through the closing `}`.
|
||||
///
|
||||
/// On premature end-of-file, returns a best-effort statement list and span.
|
||||
/// On premature end-of-file, reports the missing `}` and returns
|
||||
/// a best-effort body.
|
||||
#[must_use]
|
||||
pub(crate) fn parse_braced_block_statements_tail(
|
||||
&mut self,
|
||||
opening_brace_position: TokenPosition,
|
||||
left_brace_position: TokenPosition,
|
||||
) -> BlockBody<'src, 'arena> {
|
||||
let mut statements = self.arena.vec();
|
||||
while let Some((token, token_position)) = self.peek_token_and_position() {
|
||||
if token == Token::RightBrace {
|
||||
self.advance(); // '}'
|
||||
let span = TokenSpan::range(opening_brace_position, token_position);
|
||||
let span = TokenSpan::range(left_brace_position, token_position);
|
||||
return BlockBody { statements, span };
|
||||
}
|
||||
self.parse_next_block_item_into(&mut statements);
|
||||
match self.parse_and_append_next_block_item(&mut statements) {
|
||||
Ok(()) => {
|
||||
// Guard against parser bugs that would otherwise leave
|
||||
// block parsing stuck on the same token.
|
||||
self.ensure_forward_progress(token_position);
|
||||
}
|
||||
// Reached EOF without a closing `}`
|
||||
self.report_error_here(ParseErrorKind::BlockMissingClosingBrace);
|
||||
Err(error) => {
|
||||
// Item-level recovery failed,
|
||||
// so escalate to the block boundary.
|
||||
let error = error.sync_error_at_matching_delimiter(self, left_brace_position);
|
||||
let error_statement = error.fallback(self);
|
||||
statements.push(error_statement);
|
||||
let span = TokenSpan::range(
|
||||
opening_brace_position,
|
||||
left_brace_position,
|
||||
self.last_consumed_position_or_start(),
|
||||
);
|
||||
return BlockBody { statements, span };
|
||||
}
|
||||
}
|
||||
}
|
||||
let eof_position = self.peek_position_or_eof();
|
||||
self.make_error_at(ParseErrorKind::BlockMissingClosingBrace, eof_position)
|
||||
.related_token("left_brace", left_brace_position)
|
||||
.report(self);
|
||||
let span = TokenSpan::range(left_brace_position, self.last_consumed_position_or_start());
|
||||
|
||||
BlockBody { statements, span }
|
||||
}
|
||||
|
||||
/// Parses one statement inside a `{ ... }` block and appends it to
|
||||
/// `statements`.
|
||||
/// Parses one statement-like item inside a `{ ... }` block and appends it
|
||||
/// to `statements`.
|
||||
///
|
||||
/// This method never consumes the closing `}` and is only meant to be
|
||||
/// called while parsing inside a block. It always appends at least one
|
||||
/// statement, even in the presence of syntax errors.
|
||||
pub(crate) fn parse_next_block_item_into(
|
||||
/// called while parsing inside a block.
|
||||
///
|
||||
/// On success, it appends exactly one statement. If neither a statement nor
|
||||
/// an expression statement can be recovered locally, it returns
|
||||
/// an unreported error so the enclosing block parser can recover at
|
||||
/// block level.
|
||||
pub(crate) fn parse_and_append_next_block_item(
|
||||
&mut self,
|
||||
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
|
||||
) {
|
||||
let mut next_statement = self.parse_statement().unwrap_or_else(|| {
|
||||
let next_expression = self.parse_expression();
|
||||
let next_expression_span = *next_expression.span();
|
||||
self.arena
|
||||
.alloc_node(Statement::Expression(next_expression), next_expression_span)
|
||||
});
|
||||
if statement_needs_semicolon(&next_statement)
|
||||
&& let Some((Token::Semicolon, semicolon_position)) = self.peek_token_and_position()
|
||||
{
|
||||
next_statement.span_mut().extend_to(semicolon_position);
|
||||
statements: &mut StatementList<'src, 'arena>,
|
||||
) -> ParseResult<'src, 'arena, ()> {
|
||||
let mut statement = match self.parse_statement() {
|
||||
Some(statement) => statement,
|
||||
None => {
|
||||
// Non-statement starters are parsed as expression statements
|
||||
self.parse_expression_statement_in_block()?
|
||||
}
|
||||
};
|
||||
if block_item_requires_semicolon(&statement) {
|
||||
match self.peek_token_and_position() {
|
||||
Some((Token::Semicolon, semicolon_position)) => {
|
||||
statement.span_mut().extend_to(semicolon_position);
|
||||
self.advance(); // ';'
|
||||
}
|
||||
statements.push(next_statement);
|
||||
}
|
||||
}
|
||||
|
||||
fn statement_needs_semicolon(statement: &Statement) -> bool {
|
||||
use Statement::{Empty, Error, Expression, Function, Label, LocalVariableDeclaration};
|
||||
match statement {
|
||||
Empty | Label(_) | Error | Function(_) => false,
|
||||
Expression(expression) => expression_needs_semicolon(expression),
|
||||
LocalVariableDeclaration { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
const fn expression_needs_semicolon(expression: &Expression) -> bool {
|
||||
use Expression::{Block, DoUntil, Error, For, ForEach, If, Switch, While};
|
||||
matches!(
|
||||
expression,
|
||||
Block { .. }
|
||||
| If { .. }
|
||||
| While { .. }
|
||||
| DoUntil { .. }
|
||||
| ForEach { .. }
|
||||
| For { .. }
|
||||
| Switch { .. }
|
||||
| Error
|
||||
// A final expression before `}` may omit `;`; this makes it
|
||||
// the block's tail value.
|
||||
//
|
||||
// On end-of-file, suppress the missing-`;` diagnostic as well.
|
||||
// The block parser will report the missing `}`,
|
||||
// and an extra semicolon error would just cascade.
|
||||
None | Some((Token::RightBrace, _)) => (),
|
||||
Some((_, unexpected_token_position)) => {
|
||||
self.make_error_at_last_consumed(
|
||||
ParseErrorKind::BlockMissingSemicolonAfterExpression,
|
||||
)
|
||||
.widen_error_span_from(statement.span().start)
|
||||
.blame_token(unexpected_token_position)
|
||||
.related("expression_span", *statement.span())
|
||||
.report(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
statements.push(statement);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_expression_statement_in_block(
|
||||
&mut self,
|
||||
) -> ParseResult<'src, 'arena, StatementRef<'src, 'arena>> {
|
||||
let expression_start_position = self.peek_position_or_eof();
|
||||
let expected_block_item_after_position = self.last_consumed_position_or_start();
|
||||
let expression_result = self.parse_expression_with_start_error(
|
||||
ParseErrorKind::BlockExpectedItem,
|
||||
expected_block_item_after_position,
|
||||
expected_block_item_after_position,
|
||||
);
|
||||
let expression = match expression_result {
|
||||
Ok(expression) => expression,
|
||||
Err(error) => {
|
||||
let expression_recovery_made_no_progress =
|
||||
self.peek_position_or_eof() == expression_start_position;
|
||||
// Without progress, a fallback statement could leave
|
||||
// the block loop stuck.
|
||||
if expression_recovery_made_no_progress {
|
||||
return self.recover_bad_block_item_start_as_error_statement(error);
|
||||
}
|
||||
error.fallback(self)
|
||||
}
|
||||
};
|
||||
let expression_span = *expression.span();
|
||||
Ok(self
|
||||
.arena
|
||||
.alloc_node(Statement::Expression(expression), expression_span))
|
||||
}
|
||||
|
||||
fn recover_bad_block_item_start_as_error_statement(
|
||||
&mut self,
|
||||
error: crate::parser::ParseError,
|
||||
) -> ParseResult<'src, 'arena, StatementRef<'src, 'arena>> {
|
||||
let position_before_statement_recovery = self.peek_position_or_eof();
|
||||
// Recover one damaged block item if possible;
|
||||
// otherwise let the enclosing block recover at its own boundary.
|
||||
let error = error.sync_error_at(self, SyncLevel::StatementTerminator);
|
||||
if self.peek_position_or_eof() != position_before_statement_recovery {
|
||||
return Ok(error.fallback(self));
|
||||
}
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
fn block_item_requires_semicolon(statement: &Statement) -> bool {
|
||||
// Control-flow and block expressions do not require a trailing semicolon
|
||||
// when used as block items.
|
||||
if let Statement::Expression(expression) = statement {
|
||||
!matches!(
|
||||
**expression,
|
||||
Expression::Block { .. }
|
||||
| Expression::If { .. }
|
||||
| Expression::While { .. }
|
||||
| Expression::DoUntil { .. }
|
||||
| Expression::ForEach { .. }
|
||||
| Expression::For { .. }
|
||||
| Expression::Switch { .. }
|
||||
| Expression::Error
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
token_position,
|
||||
),
|
||||
Token::LeftParenthesis => self.parse_parenthesized_expression_tail(token_position),
|
||||
Token::LeftBrace => self.parse_block_tail(token_position),
|
||||
Token::LeftBrace => self.parse_block_body_tail(token_position),
|
||||
Token::Keyword(keyword) => {
|
||||
match self.try_parse_keyword_primary(keyword, token_position) {
|
||||
Some(keyword_expression) => keyword_expression,
|
||||
|
||||
@ -96,7 +96,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
// at statement sync level.
|
||||
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
|
||||
.widen_error_span_from(case_position)
|
||||
.sync_error_until(self, crate::parser::SyncLevel::Statement)
|
||||
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
|
||||
.report_error(self);
|
||||
}
|
||||
let mut body = self.arena.vec();
|
||||
@ -115,7 +115,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
self.advance(); // 'default'
|
||||
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
|
||||
.widen_error_span_from(default_position)
|
||||
.sync_error_until(self, crate::parser::SyncLevel::Statement)
|
||||
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
|
||||
.report_error(self);
|
||||
self.parse_switch_arm_body(statements);
|
||||
}
|
||||
@ -129,7 +129,7 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
match token {
|
||||
Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => break,
|
||||
_ => {
|
||||
self.parse_next_block_item_into(statements);
|
||||
self.parse_and_append_next_block_item(statements);
|
||||
self.ensure_forward_progress(token_position);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,9 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
/// Parses a single statement.
|
||||
///
|
||||
/// Does not consume a trailing `;` except for [`Statement::Empty`].
|
||||
/// The caller handles semicolons. Returns [`Some`] if a statement is
|
||||
/// The caller handles semicolons (WRONG NOW - WE MUST HANDLE THEM). Returns [`Some`] if a statement is
|
||||
/// recognized; otherwise [`None`].
|
||||
/// ALSO WE SPECIFICALLY DONT HANDLE EXPRESSION TYPE STATEMENTS
|
||||
#[must_use]
|
||||
pub(crate) fn parse_statement(&mut self) -> Option<StatementRef<'src, 'arena>> {
|
||||
let Some((token, lexeme, position)) = self.peek_token_lexeme_and_position() else {
|
||||
|
||||
@ -50,7 +50,10 @@ pub enum SyncLevel {
|
||||
///
|
||||
/// Includes `;` and keywords that begin standalone statements /
|
||||
/// statement-like control-flow forms.
|
||||
Statement,
|
||||
StatementStart,
|
||||
|
||||
/// Statement terminator `;`.
|
||||
StatementTerminator,
|
||||
|
||||
/// Start of a `switch` arm.
|
||||
///
|
||||
@ -77,7 +80,7 @@ impl SyncLevel {
|
||||
use crate::lexer::Keyword;
|
||||
use SyncLevel::{
|
||||
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart,
|
||||
ExpressionStart, ListSeparator, Statement, SwitchArmStart,
|
||||
ExpressionStart, ListSeparator, StatementStart, StatementTerminator, SwitchArmStart,
|
||||
};
|
||||
|
||||
match token {
|
||||
@ -88,8 +91,7 @@ impl SyncLevel {
|
||||
Token::RightBracket => Some(CloseBracket),
|
||||
|
||||
// Statement-level boundaries
|
||||
Token::Semicolon
|
||||
| Token::Keyword(
|
||||
Token::Keyword(
|
||||
Keyword::If
|
||||
| Keyword::Else
|
||||
| Keyword::Switch
|
||||
@ -102,7 +104,9 @@ impl SyncLevel {
|
||||
| Keyword::Break
|
||||
| Keyword::Continue
|
||||
| Keyword::Local,
|
||||
) => Some(Statement),
|
||||
) => Some(StatementStart),
|
||||
|
||||
Token::Semicolon => Some(StatementTerminator),
|
||||
|
||||
// Switch-specific stronger boundary
|
||||
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchArmStart),
|
||||
|
||||
455
rottlib/tests/parser_diagnostics/block_items.rs
Normal file
455
rottlib/tests/parser_diagnostics/block_items.rs
Normal file
@ -0,0 +1,455 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) const P0025_FIXTURES: &[Fixture] = &[
|
||||
Fixture {
|
||||
label: "files/P0025_01.uc",
|
||||
source: "{\n local int Count;\n Count = Count + 1 UpdateHud();\n DrawHud(CanvasRef);\n}\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0025_02.uc",
|
||||
source: "{\n local float XL;\n C.TextSize(LevelTitle, XL, YL)\n C.SetPos(0, 0);\n C.DrawText(LevelTitle);\n}\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0025_03.uc",
|
||||
source: "{\n local bool bReady;\n bReady = CheckReady()\n if (bReady) { StartMatch(); }\n NotifyReady();\n}\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0025_04.uc",
|
||||
source: "{\n local int I;\n Scores[I] = Scores[I] + 1\n I++;\n RefreshScores();\n}\n",
|
||||
},
|
||||
];
|
||||
|
||||
pub(super) const P0026_FIXTURES: &[Fixture] = &[
|
||||
Fixture {
|
||||
label: "files/P0026_01.uc",
|
||||
source: "{\n local int Count;\n Count = 0;\n Count++;\n UpdateHud();\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0026_02.uc",
|
||||
source: "{\n local bool bReady;\n bReady = CheckReady();\n if (bReady) { StartMatch(); }\n NotifyReady();\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0026_03.uc",
|
||||
source: "{ local float XL; do { C.TextSize(LevelTitle, XL, YL); } until (XL < C.ClipX) C.SetPos(0, 0); C.DrawText(LevelTitle);",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0026_04.uc",
|
||||
source: "{\n local int Count;\n Count = Count + 1;\n UpdateHud();\n Count\n",
|
||||
},
|
||||
];
|
||||
|
||||
pub(super) const P0025_P0026_MIXED_FIXTURES: &[Fixture] = &[
|
||||
Fixture {
|
||||
label: "files/P_mixed_01.uc",
|
||||
source: "{ local int Count; Count = Count + 1 UpdateHud(); DrawHud(CanvasRef);",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P_mixed_02.uc",
|
||||
source: "{\n local bool bReady;\n bReady = CheckReady()\n if (bReady) { StartMatch(); }\n NotifyReady();\n",
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn check_p0025_fixtures() {
|
||||
let runs = run_fixtures(P0025_FIXTURES);
|
||||
|
||||
assert_eq!(runs.get("files/P0025_01.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0025_02.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0025_03.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0025_04.uc").unwrap().len(), 1);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0025_01.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `;` after expression statement",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0025"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(21),
|
||||
end: TokenPosition(21),
|
||||
},
|
||||
message: "expected `;` before `UpdateHud`",
|
||||
}),
|
||||
secondary_labels: &[ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(11),
|
||||
end: TokenPosition(19),
|
||||
},
|
||||
message: "expression statement",
|
||||
}],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0025_02.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `;` after expression statement",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0025"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(25),
|
||||
end: TokenPosition(25),
|
||||
},
|
||||
message: "expected `;` before `C`",
|
||||
}),
|
||||
secondary_labels: &[ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(22),
|
||||
end: TokenPosition(22),
|
||||
},
|
||||
message: "expression statement ends here",
|
||||
}],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0025_03.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `;` after expression statement",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0025"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(20),
|
||||
end: TokenPosition(20),
|
||||
},
|
||||
message: "expected `;` before `if`",
|
||||
}),
|
||||
secondary_labels: &[ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(17),
|
||||
end: TokenPosition(17),
|
||||
},
|
||||
message: "expression statement ends here",
|
||||
}],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0025_04.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `;` after expression statement",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0025"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(28),
|
||||
end: TokenPosition(28),
|
||||
},
|
||||
message: "expected `;` before `I`",
|
||||
}),
|
||||
secondary_labels: &[ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(25),
|
||||
end: TokenPosition(25),
|
||||
},
|
||||
message: "expression statement ends here",
|
||||
}],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_p0026_fixtures() {
|
||||
let runs = run_fixtures(P0026_FIXTURES);
|
||||
|
||||
assert_eq!(runs.get("files/P0026_01.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0026_02.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0026_03.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0026_04.uc").unwrap().len(), 1);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0026_01.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `}` to close block",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0026"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(0),
|
||||
end: TokenPosition(29),
|
||||
},
|
||||
message: "expected `}` before end of file",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0026_02.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `}` to close block",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0026"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(0),
|
||||
end: TokenPosition(42),
|
||||
},
|
||||
message: "expected `}` before end of file",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0026_03.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `}` to close block",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0026"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(59),
|
||||
end: TokenPosition(59),
|
||||
},
|
||||
message: "expected `}` before end of file",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0026_04.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `}` to close block",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0026"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(0),
|
||||
end: TokenPosition(31),
|
||||
},
|
||||
message: "expected `}` before end of file",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_p0025_mixed_fixtures() {
|
||||
let runs = run_fixtures(P0025_P0026_MIXED_FIXTURES);
|
||||
|
||||
assert_eq!(runs.get("files/P_mixed_01.uc").unwrap().len(), 2);
|
||||
assert_eq!(runs.get("files/P_mixed_02.uc").unwrap().len(), 2);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_by_code("files/P_mixed_01.uc", "P0025"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `;` after expression statement",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0025"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(19),
|
||||
end: TokenPosition(19),
|
||||
},
|
||||
message: "expected `;` before `UpdateHud`",
|
||||
}),
|
||||
secondary_labels: &[ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(9),
|
||||
end: TokenPosition(17),
|
||||
},
|
||||
message: "expression statement",
|
||||
}],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_by_code("files/P_mixed_02.uc", "P0025"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `;` after expression statement",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0025"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(20),
|
||||
end: TokenPosition(20),
|
||||
},
|
||||
message: "expected `;` before `if`",
|
||||
}),
|
||||
secondary_labels: &[ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(17),
|
||||
end: TokenPosition(17),
|
||||
},
|
||||
message: "expression statement ends here",
|
||||
}],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_p0026_mixed_fixtures() {
|
||||
let runs = run_fixtures(P0025_P0026_MIXED_FIXTURES);
|
||||
|
||||
assert_eq!(runs.get("files/P_mixed_01.uc").unwrap().len(), 2);
|
||||
assert_eq!(runs.get("files/P_mixed_02.uc").unwrap().len(), 2);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_by_code("files/P_mixed_01.uc", "P0026"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `}` to close block",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0026"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(29),
|
||||
end: TokenPosition(29),
|
||||
},
|
||||
message: "expected `}` before end of file",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_by_code("files/P_mixed_02.uc", "P0026"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "missing `}` to close block",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0026"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(0),
|
||||
end: TokenPosition(41),
|
||||
},
|
||||
message: "expected `}` before end of file",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) const P0027_FIXTURES: &[Fixture] = &[
|
||||
Fixture {
|
||||
label: "files/P0027_01.uc",
|
||||
source: "{\n local bool bReady;\n bReady = CheckReady();\n else { StartMatch(); }\n NotifyReady();\n}\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0027_02.uc",
|
||||
source: "{ local int Count; Count = 3; case 3: Count++; UpdateHud();}",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0027_03.uc",
|
||||
source: "{\n local bool bDone;\n bDone = false;\n until (bDone)\n TickWork();\n}\n",
|
||||
},
|
||||
Fixture {
|
||||
label: "files/P0027_04.uc",
|
||||
source: "{\n local int Count;\n Count = 0;\n #exec TEXTURE IMPORT NAME=Bad FILE=Bad.bmp\n Count++;\n}\n",
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn check_p0027_fixtures() {
|
||||
let runs = run_fixtures(P0027_FIXTURES);
|
||||
|
||||
assert_eq!(runs.get("files/P0027_01.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0027_02.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0027_03.uc").unwrap().len(), 1);
|
||||
assert_eq!(runs.get("files/P0027_04.uc").unwrap().len(), 1);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0027_01.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "expected statement or expression, found `else`",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0027"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(21),
|
||||
end: TokenPosition(21),
|
||||
},
|
||||
message: "unexpected `else`",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0027_02.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "expected statement or expression, found `case`",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0027"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(16),
|
||||
end: TokenPosition(16),
|
||||
},
|
||||
message: "unexpected `case`",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0027_03.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "expected statement or expression, found `until`",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0027"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(19),
|
||||
end: TokenPosition(19),
|
||||
},
|
||||
message: "unexpected `until`",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0027_04.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "expected statement or expression, found `#exec` directive",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0027"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
span: TokenSpan {
|
||||
start: TokenPosition(19),
|
||||
end: TokenPosition(19),
|
||||
},
|
||||
message: "`#exec` directives are not allowed in a statement block",
|
||||
}),
|
||||
secondary_labels: &[],
|
||||
help: None,
|
||||
notes: &[],
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -5,6 +5,7 @@ use rottlib::diagnostics::{Diagnostic, Severity};
|
||||
use rottlib::lexer::{TokenPosition, TokenSpan, TokenizedFile};
|
||||
use rottlib::parser::Parser;
|
||||
|
||||
mod block_items;
|
||||
mod control_flow_expressions;
|
||||
mod primary_expressions;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user