Improve switch's diagnostics
This commit is contained in:
parent
e29ffb2a9c
commit
f695f8a52e
@ -16,43 +16,163 @@ use rottlib::parser::Parser;
|
|||||||
/// Keep these small: the goal is to inspect lexer diagnostics and delimiter
|
/// Keep these small: the goal is to inspect lexer diagnostics and delimiter
|
||||||
/// recovery behavior, not full parser behavior.
|
/// recovery behavior, not full parser behavior.
|
||||||
const TEST_CASES: &[(&str, &str)] = &[
|
const TEST_CASES: &[(&str, &str)] = &[
|
||||||
// P0031 - FunctionCallArgumentMissingComma
|
// P0034 - SwitchMissingBody
|
||||||
|
("files/P0034_01.uc", "switch(A) local\n"),
|
||||||
|
("files/P0034_02.uc", "switch\n(A)\nvar"),
|
||||||
|
("files/P0034_03.uc", "switch(\n A\n)\n"),
|
||||||
|
("files/P0034_04.uc", "switch\n(\n A\n)\n"),
|
||||||
|
("files/P0034_05.uc", "switch(A)\ncase 1:\n"),
|
||||||
|
|
||||||
|
// P0035 - SwitchTopLevelItemNotCase
|
||||||
|
("files/P0035_01.uc", "switch(A) {\n Log(\"bad\");\n}\n"),
|
||||||
(
|
(
|
||||||
"files/P0031_01.uc",
|
"files/P0035_02.uc",
|
||||||
"{\n Func(A B);\n Log(\"after\");\n}\n",
|
"switch\n(A)\n{\n Log(\"bad\");\n Log(\"worse\");\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
("files/P0035_03.uc", "switch(A) {\n 123;\n default:\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0035_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n if (A) {}\n case 1:\n}\n",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"files/P0031_02.uc",
|
"files/P0035_05.uc",
|
||||||
"{\n Func\n (A 123);\n Log(\"after\");\n}\n",
|
"switch(A) {\n {\n Log(\"nested\");\n }\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0036 - SwitchCaseMissingColon
|
||||||
|
(
|
||||||
|
"files/P0036_01.uc",
|
||||||
|
"switch(A) {\n case 1\n case 2:\n}\n",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"files/P0031_03.uc",
|
"files/P0036_02.uc",
|
||||||
"{\n Func(\n A\n new SomeClass\n );\n Log(\"after\");\n}\n",
|
"switch\n(A)\n{\n case\n 1\n default:\n}\n",
|
||||||
),
|
|
||||||
// P0032 - FunctionCallMissingClosingParenthesis
|
|
||||||
("files/P0032_01.uc", "Func("),
|
|
||||||
("files/P0032_02.uc", "Func\n(\n A,"),
|
|
||||||
("files/P0032_03.uc", "Func(A,\n B,"),
|
|
||||||
(
|
|
||||||
"files/P0032_04.uc",
|
|
||||||
"{\n Func\n (\n A,\n B,\n",
|
|
||||||
),
|
|
||||||
// P0033 - FunctionCallUnexpectedTokenInArgumentList
|
|
||||||
(
|
|
||||||
"files/P0033_01.uc",
|
|
||||||
"{\n Func(A #, B);\n Log(\"after\");\n}\n",
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"files/P0033_02.uc",
|
"files/P0036_03.uc",
|
||||||
"{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n",
|
"switch(A) {\n case (A)\n case B:\n}\n",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"files/P0033_03.uc",
|
"files/P0036_04.uc",
|
||||||
"{\n Func(\n A ?\n , B\n );\n Log(\"after\");\n}\n",
|
"switch\n(\n A\n)\n{\n case\n A + B\n default\n :\n}\n",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"files/P0033_04.uc",
|
"files/P0036_05.uc",
|
||||||
"{\n Func\n (\n A\n #,\n B\n );\n Log(\"after\");\n}\n",
|
"switch(A) {\n case Foo.Bar(Baz)\n case Other:\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0037 - SwitchDefaultMissingColon
|
||||||
|
(
|
||||||
|
"files/P0037_01.uc",
|
||||||
|
"switch(A) {\n default\n if (A) {}\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0037_02.uc",
|
||||||
|
"switch\n(A)\n{\n default\n while (A) {}\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0037_03.uc",
|
||||||
|
"switch(A) {\n default\n for (;;) {}\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0037_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n default\n switch(B) {\n case 1:\n }\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0037_05.uc",
|
||||||
|
"switch(A) {\n default\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0038 - SwitchDuplicateDefault
|
||||||
|
(
|
||||||
|
"files/P0038_01.uc",
|
||||||
|
"switch(A) {\n default:\n default:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0038_02.uc",
|
||||||
|
"switch\n(A)\n{\n default\n :\n default\n :\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0038_03.uc",
|
||||||
|
"switch(A) {\n default:\n default:\n default:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0038_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n default:\n Log(\"first\");\n default:\n Log(\"second\");\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0039 - SwitchCasesAfterDefault
|
||||||
|
(
|
||||||
|
"files/P0039_01.uc",
|
||||||
|
"switch(A) {\n default:\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0039_02.uc",
|
||||||
|
"switch\n(A)\n{\n default\n :\n case\n 1\n :\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0039_03.uc",
|
||||||
|
"switch(A) {\n default:\n case 1:\n case 2:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0039_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n default:\n case 1:\n Log(\"one\");\n case 2:\n Log(\"two\");\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0039_05.uc",
|
||||||
|
"switch(A) {\n default:\n Log(\"done\");\n case 1:\n case 2:\n Log(\"stacked\");\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0040 - SwitchMissingClosingBrace
|
||||||
|
("files/P0040_01.uc", "switch(A) {\n"),
|
||||||
|
("files/P0040_02.uc", "switch(A) {\n case 1:\n"),
|
||||||
|
("files/P0040_03.uc", "switch\n(A)\n{\n default:\n"),
|
||||||
|
(
|
||||||
|
"files/P0040_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n case 1:\n case 2:\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0040_05.uc",
|
||||||
|
"switch(A) {\n case 1:\n Log(\"body\");\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0041 - SwitchCaseMissingExpression
|
||||||
|
("files/P0041_01.uc", "switch(A) {\n case:\n}\n"),
|
||||||
|
("files/P0041_02.uc", "switch\n(A)\n{\n case\n :\n}\n"),
|
||||||
|
("files/P0041_03.uc", "switch(A) {\n case:\n default:\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0041_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n case\n :\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0041_05.uc",
|
||||||
|
"switch(A) {\n case\n :\n default:\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0042 - SwitchCaseExpressionInvalidStart
|
||||||
|
("files/P0042_01.uc", "switch(A) {\n case *:\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0042_02.uc",
|
||||||
|
"switch\n(A)\n{\n case\n =\n :\n}\n",
|
||||||
|
),
|
||||||
|
("files/P0042_03.uc", "switch(A) {\n case &&:\n default:\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0042_04.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n case\n .\n :\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0042_05.uc",
|
||||||
|
"switch(A) {\n case ]:\n}\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// Mixed / intentional cascades
|
||||||
|
(
|
||||||
|
"files/P0042_cascade_01.uc",
|
||||||
|
"switch(A) {\n case *\n case 1:\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0042_cascade_02.uc",
|
||||||
|
"switch\n(\n A\n)\n{\n case\n =\n default:\n}\n",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ mod block_items;
|
|||||||
mod control_flow_expressions;
|
mod control_flow_expressions;
|
||||||
mod primary_expressions;
|
mod primary_expressions;
|
||||||
mod selector_expressions;
|
mod selector_expressions;
|
||||||
|
mod switch_expressions;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum FoundAt<'src> {
|
enum FoundAt<'src> {
|
||||||
@ -78,6 +79,7 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
|
|||||||
use control_flow_expressions::*;
|
use control_flow_expressions::*;
|
||||||
use primary_expressions::*;
|
use primary_expressions::*;
|
||||||
use selector_expressions::*;
|
use selector_expressions::*;
|
||||||
|
use switch_expressions::*;
|
||||||
match error.kind {
|
match error.kind {
|
||||||
// primary_expressions.rs
|
// primary_expressions.rs
|
||||||
ParseErrorKind::ParenthesizedExpressionInvalidStart => {
|
ParseErrorKind::ParenthesizedExpressionInvalidStart => {
|
||||||
@ -165,6 +167,28 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
|
|||||||
ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList => {
|
ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList => {
|
||||||
diagnostic_function_call_unexpected_token_in_argument_list(error, file)
|
diagnostic_function_call_unexpected_token_in_argument_list(error, file)
|
||||||
}
|
}
|
||||||
|
// switch_expressions.rs
|
||||||
|
ParseErrorKind::SwitchMissingBody => diagnostic_switch_missing_body(error, file),
|
||||||
|
ParseErrorKind::SwitchTopLevelItemNotCase => {
|
||||||
|
diagnostic_switch_top_level_item_not_case(error, file)
|
||||||
|
}
|
||||||
|
ParseErrorKind::SwitchCaseMissingColon => diagnostic_switch_case_missing_colon(error, file),
|
||||||
|
ParseErrorKind::SwitchDefaultMissingColon => {
|
||||||
|
diagnostic_switch_default_missing_colon(error, file)
|
||||||
|
}
|
||||||
|
ParseErrorKind::SwitchDuplicateDefault => diagnostic_switch_duplicate_default(error, file),
|
||||||
|
ParseErrorKind::SwitchCasesAfterDefault => {
|
||||||
|
diagnostic_switch_cases_after_default(error, file)
|
||||||
|
}
|
||||||
|
ParseErrorKind::SwitchMissingClosingBrace => {
|
||||||
|
diagnostic_switch_missing_closing_brace(error, file)
|
||||||
|
}
|
||||||
|
ParseErrorKind::SwitchCaseMissingExpression => {
|
||||||
|
diagnostic_switch_case_missing_expression(error, file)
|
||||||
|
}
|
||||||
|
ParseErrorKind::SwitchCaseExpressionInvalidStart => {
|
||||||
|
diagnostic_switch_case_expression_invalid_start(error, file)
|
||||||
|
}
|
||||||
|
|
||||||
_ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind))
|
_ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind))
|
||||||
.primary_label(error.covered_span, "happened here")
|
.primary_label(error.covered_span, "happened here")
|
||||||
|
|||||||
@ -0,0 +1,417 @@
|
|||||||
|
use super::{Diagnostic, DiagnosticBuilder, FoundAt, found_at, should_show_context_label};
|
||||||
|
use crate::lexer::{TokenSpan, TokenizedFile};
|
||||||
|
use crate::parser::ParseError;
|
||||||
|
|
||||||
|
const SWITCH_KEYWORD: &str = "switch_keyword";
|
||||||
|
const LEFT_BRACE: &str = "left_brace";
|
||||||
|
const CASE_KEYWORD: &str = "case_keyword";
|
||||||
|
const DEFAULT_KEYWORD: &str = "default_keyword";
|
||||||
|
const CASE_EXPRESSION: &str = "case_expression";
|
||||||
|
const FIRST_DEFAULT: &str = "first_default";
|
||||||
|
|
||||||
|
const DUPLICATE_DEFAULT_PREFIX: &str = "duplicate_default";
|
||||||
|
const CASE_AFTER_DEFAULT_PREFIX: &str = "case_after_default";
|
||||||
|
|
||||||
|
fn related_span(error: &ParseError, label: &str) -> Option<TokenSpan> {
|
||||||
|
error.related_spans.get(label).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context_span_if_useful(
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
context_span: Option<TokenSpan>,
|
||||||
|
blame_span: TokenSpan,
|
||||||
|
) -> Option<TokenSpan> {
|
||||||
|
context_span.filter(|span| should_show_context_label(file, *span, blame_span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_span_with_context(
|
||||||
|
context_span: Option<TokenSpan>,
|
||||||
|
blame_span: TokenSpan,
|
||||||
|
) -> TokenSpan {
|
||||||
|
match context_span {
|
||||||
|
Some(context_span) => TokenSpan {
|
||||||
|
start: context_span.start,
|
||||||
|
end: blame_span.end,
|
||||||
|
},
|
||||||
|
None => blame_span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_span(left: TokenSpan, right: TokenSpan) -> bool {
|
||||||
|
left.start == right.start && left.end == right.end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_keyword_context_span(
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
error: &ParseError,
|
||||||
|
blame_span: TokenSpan,
|
||||||
|
left_brace_span: Option<TokenSpan>,
|
||||||
|
) -> Option<TokenSpan> {
|
||||||
|
let switch_keyword_span = related_span(error, SWITCH_KEYWORD)?;
|
||||||
|
|
||||||
|
if file.same_line(switch_keyword_span.start, blame_span.end) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(left_brace_span) = left_brace_span
|
||||||
|
&& file.same_line(switch_keyword_span.start, left_brace_span.start)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(switch_keyword_span)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_case_label_context_span(
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
error: &ParseError,
|
||||||
|
blame_span: TokenSpan,
|
||||||
|
) -> Option<TokenSpan> {
|
||||||
|
let case_keyword_span = related_span(error, CASE_KEYWORD)?;
|
||||||
|
|
||||||
|
let case_label_span = match related_span(error, CASE_EXPRESSION) {
|
||||||
|
Some(case_expression_span) => TokenSpan {
|
||||||
|
start: case_keyword_span.start,
|
||||||
|
end: case_expression_span.end,
|
||||||
|
},
|
||||||
|
None => case_keyword_span,
|
||||||
|
};
|
||||||
|
|
||||||
|
context_span_if_useful(file, Some(case_label_span), blame_span)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numbered_related_count(error: &ParseError, prefix: &str) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
for index in 1.. {
|
||||||
|
let label = format!("{}_{}", prefix, index);
|
||||||
|
if related_span(error, &label).is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_numbered_related_labels(
|
||||||
|
mut builder: DiagnosticBuilder,
|
||||||
|
error: &ParseError,
|
||||||
|
prefix: &str,
|
||||||
|
primary_span: TokenSpan,
|
||||||
|
first_text: &'static str,
|
||||||
|
later_text: &'static str,
|
||||||
|
) -> DiagnosticBuilder {
|
||||||
|
for index in 1.. {
|
||||||
|
let label = format!("{}_{}", prefix, index);
|
||||||
|
let Some(span) = related_span(error, &label) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if same_span(span, primary_span) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = if index == 1 { first_text } else { later_text };
|
||||||
|
builder = builder.secondary_label(span, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0034
|
||||||
|
pub(super) fn diagnostic_switch_missing_body(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let switch_context_span =
|
||||||
|
context_span_if_useful(file, related_span(&error, SWITCH_KEYWORD), blame_span);
|
||||||
|
let primary_span = primary_span_with_context(switch_context_span, blame_span);
|
||||||
|
|
||||||
|
let primary_text = match found_at(file, blame_span.start) {
|
||||||
|
FoundAt::Token(token_text) => format!("expected `{{` before `{}`", token_text),
|
||||||
|
FoundAt::EndOfFile => "expected `{` after the switch expression".to_string(),
|
||||||
|
FoundAt::Unknown => "expected `{` here".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
DiagnosticBuilder::error("missing `{` to start `switch` body")
|
||||||
|
.primary_label(primary_span, primary_text)
|
||||||
|
.code("P0034")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0035
|
||||||
|
pub(super) fn diagnostic_switch_top_level_item_not_case(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let left_brace_span = related_span(&error, LEFT_BRACE);
|
||||||
|
|
||||||
|
let left_brace_context_span = context_span_if_useful(file, left_brace_span, blame_span);
|
||||||
|
let switch_keyword_context_span =
|
||||||
|
switch_keyword_context_span(file, &error, blame_span, left_brace_span);
|
||||||
|
|
||||||
|
let primary_text = if matches!(found_at(file, blame_span.start), FoundAt::Token("{")) {
|
||||||
|
"this block must be inside a `case` or `default` section"
|
||||||
|
} else if related_span(&error, "multiple_items").is_some() {
|
||||||
|
"these statements must be inside a `case` or `default` section"
|
||||||
|
} else {
|
||||||
|
"this statement must be inside a `case` or `default` section"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder =
|
||||||
|
DiagnosticBuilder::error("expected `case` or `default` section label in switch body");
|
||||||
|
|
||||||
|
if let Some(switch_keyword_context_span) = switch_keyword_context_span {
|
||||||
|
builder = builder.secondary_label(switch_keyword_context_span, "`switch` starts here");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(left_brace_context_span) = left_brace_context_span {
|
||||||
|
builder = builder.secondary_label(left_brace_context_span, "switch body starts here");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, primary_text)
|
||||||
|
.code("P0035")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0036
|
||||||
|
pub(super) fn diagnostic_switch_case_missing_colon(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let case_label_context_span = switch_case_label_context_span(file, &error, blame_span);
|
||||||
|
|
||||||
|
let primary_text = match found_at(file, blame_span.start) {
|
||||||
|
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 `case` label");
|
||||||
|
|
||||||
|
if let Some(case_label_context_span) = case_label_context_span {
|
||||||
|
builder = builder.secondary_label(
|
||||||
|
case_label_context_span,
|
||||||
|
"this `case` label needs a trailing `:`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, primary_text)
|
||||||
|
.code("P0036")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0037
|
||||||
|
pub(super) fn diagnostic_switch_default_missing_colon(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let default_context_span =
|
||||||
|
context_span_if_useful(file, related_span(&error, DEFAULT_KEYWORD), blame_span);
|
||||||
|
|
||||||
|
let primary_text = match found_at(file, blame_span.start) {
|
||||||
|
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 `default`");
|
||||||
|
|
||||||
|
if let Some(default_context_span) = default_context_span {
|
||||||
|
builder = builder.secondary_label(
|
||||||
|
default_context_span,
|
||||||
|
"this `default` label needs a trailing `:`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, primary_text)
|
||||||
|
.code("P0037")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0038
|
||||||
|
pub(super) fn diagnostic_switch_duplicate_default(
|
||||||
|
error: ParseError,
|
||||||
|
_file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let duplicate_count = numbered_related_count(&error, DUPLICATE_DEFAULT_PREFIX);
|
||||||
|
|
||||||
|
let title = if duplicate_count > 1 {
|
||||||
|
"multiple `default` sections in switch"
|
||||||
|
} else {
|
||||||
|
"duplicate `default` section in switch"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = DiagnosticBuilder::error(title);
|
||||||
|
|
||||||
|
if let Some(first_default_span) = related_span(&error, FIRST_DEFAULT)
|
||||||
|
&& !same_span(first_default_span, blame_span)
|
||||||
|
{
|
||||||
|
builder = builder.secondary_label(first_default_span, "first `default` section is here");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = add_numbered_related_labels(
|
||||||
|
builder,
|
||||||
|
&error,
|
||||||
|
DUPLICATE_DEFAULT_PREFIX,
|
||||||
|
blame_span,
|
||||||
|
"duplicate `default` section",
|
||||||
|
"another duplicate `default` section",
|
||||||
|
);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, "duplicate `default` section")
|
||||||
|
.code("P0038")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0039
|
||||||
|
pub(super) fn diagnostic_switch_cases_after_default(
|
||||||
|
error: ParseError,
|
||||||
|
_file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let case_after_default_count = numbered_related_count(&error, CASE_AFTER_DEFAULT_PREFIX);
|
||||||
|
|
||||||
|
let title = if case_after_default_count > 1 {
|
||||||
|
"multiple `case` sections appear after `default`"
|
||||||
|
} else {
|
||||||
|
"`case` section appears after `default`"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = DiagnosticBuilder::error(title);
|
||||||
|
|
||||||
|
if let Some(first_default_span) = related_span(&error, FIRST_DEFAULT)
|
||||||
|
&& !same_span(first_default_span, blame_span)
|
||||||
|
{
|
||||||
|
builder = builder.secondary_label(
|
||||||
|
first_default_span,
|
||||||
|
"`default` must be the last section in this switch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = add_numbered_related_labels(
|
||||||
|
builder,
|
||||||
|
&error,
|
||||||
|
CASE_AFTER_DEFAULT_PREFIX,
|
||||||
|
blame_span,
|
||||||
|
"`case` after `default`",
|
||||||
|
"another `case` after `default`",
|
||||||
|
);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, "`case` after `default`")
|
||||||
|
.code("P0039")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0040
|
||||||
|
pub(super) fn diagnostic_switch_missing_closing_brace(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let left_brace_span = related_span(&error, LEFT_BRACE);
|
||||||
|
|
||||||
|
let left_brace_context_span = context_span_if_useful(file, left_brace_span, blame_span);
|
||||||
|
let switch_keyword_context_span =
|
||||||
|
switch_keyword_context_span(file, &error, blame_span, left_brace_span);
|
||||||
|
let primary_span = primary_span_with_context(left_brace_context_span, blame_span);
|
||||||
|
|
||||||
|
let primary_text = match found_at(file, blame_span.start) {
|
||||||
|
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 `}` to close switch body");
|
||||||
|
|
||||||
|
if let Some(switch_keyword_context_span) = switch_keyword_context_span {
|
||||||
|
builder = builder.secondary_label(switch_keyword_context_span, "`switch` starts here");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(primary_span, primary_text)
|
||||||
|
.code("P0040")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0041
|
||||||
|
pub(super) fn diagnostic_switch_case_missing_expression(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let case_context_span =
|
||||||
|
context_span_if_useful(file, related_span(&error, CASE_KEYWORD), blame_span);
|
||||||
|
|
||||||
|
let primary_text = match found_at(file, blame_span.start) {
|
||||||
|
FoundAt::Token(":") => "expected expression before `:`".to_string(),
|
||||||
|
FoundAt::Token(token_text) => format!("expected expression before `{}`", token_text),
|
||||||
|
FoundAt::EndOfFile => "expected expression before end of file".to_string(),
|
||||||
|
FoundAt::Unknown => "expected expression here".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = DiagnosticBuilder::error("missing expression after `case`");
|
||||||
|
|
||||||
|
if let Some(case_context_span) = case_context_span {
|
||||||
|
builder = builder.secondary_label(
|
||||||
|
case_context_span,
|
||||||
|
"after this `case`, an expression was expected",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, primary_text)
|
||||||
|
.code("P0041")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0042
|
||||||
|
pub(super) fn diagnostic_switch_case_expression_invalid_start(
|
||||||
|
error: ParseError,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let blame_span = error.blame_span;
|
||||||
|
let case_context_span =
|
||||||
|
context_span_if_useful(file, related_span(&error, CASE_KEYWORD), blame_span);
|
||||||
|
|
||||||
|
let found = found_at(file, blame_span.start);
|
||||||
|
|
||||||
|
let title = match found {
|
||||||
|
FoundAt::Token(token_text) => {
|
||||||
|
format!("expected expression after `case`, found `{}`", token_text)
|
||||||
|
}
|
||||||
|
FoundAt::EndOfFile => "expected expression after `case`, found end of file".to_string(),
|
||||||
|
FoundAt::Unknown => "expected expression after `case`".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_text = match found {
|
||||||
|
FoundAt::Token(token_text) => format!("unexpected `{}`", token_text),
|
||||||
|
FoundAt::EndOfFile => "reached end of file here".to_string(),
|
||||||
|
FoundAt::Unknown => "expected expression here".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = DiagnosticBuilder::error(title);
|
||||||
|
|
||||||
|
if let Some(case_context_span) = case_context_span {
|
||||||
|
builder = builder.secondary_label(
|
||||||
|
case_context_span,
|
||||||
|
"after this `case`, an expression was expected",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.primary_label(blame_span, primary_text)
|
||||||
|
.code("P0042")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
@ -8,7 +8,22 @@ use std::collections::HashMap;
|
|||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
const INDENT: &str = " ";
|
const INDENT: &str = " ";
|
||||||
|
/*
|
||||||
|
error[P0034]: missing `{` to start `switch` body
|
||||||
|
in file: files/P0034_04.uc
|
||||||
|
1 | ╭switch
|
||||||
|
| │------ `switch` starts here
|
||||||
|
2 | ╭ │(
|
||||||
|
3 | │ │ A
|
||||||
|
4 | │ │)
|
||||||
|
| │ ╰─^ expected `{` before end of file
|
||||||
|
| ╰ ^ switch selector is here
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Outmost guideline isn't reaching ^!. And should it be `^` even?
|
||||||
|
*/
|
||||||
/*
|
/*
|
||||||
error: expected one of `,`, `:`, or `}`, found `token_to`
|
error: expected one of `,`, `:`, or `}`, found `token_to`
|
||||||
--> rottlib/src/ast/mod.rs:80:13
|
--> rottlib/src/ast/mod.rs:80:13
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{lexer::TokenSpan, lexer::TokenPosition};
|
use crate::{lexer::TokenPosition, lexer::TokenSpan};
|
||||||
|
|
||||||
/// Internal parse error kinds.
|
/// Internal parse error kinds.
|
||||||
///
|
///
|
||||||
@ -82,6 +82,24 @@ pub enum ParseErrorKind {
|
|||||||
FunctionCallMissingClosingParenthesis,
|
FunctionCallMissingClosingParenthesis,
|
||||||
/// P0033
|
/// P0033
|
||||||
FunctionCallUnexpectedTokenInArgumentList,
|
FunctionCallUnexpectedTokenInArgumentList,
|
||||||
|
/// P0034
|
||||||
|
SwitchMissingBody,
|
||||||
|
/// P0035
|
||||||
|
SwitchTopLevelItemNotCase,
|
||||||
|
/// P0036
|
||||||
|
SwitchCaseMissingColon,
|
||||||
|
/// P0037
|
||||||
|
SwitchDefaultMissingColon,
|
||||||
|
/// P0038
|
||||||
|
SwitchDuplicateDefault,
|
||||||
|
/// P0039
|
||||||
|
SwitchCasesAfterDefault,
|
||||||
|
/// P0040
|
||||||
|
SwitchMissingClosingBrace,
|
||||||
|
/// P0041
|
||||||
|
SwitchCaseMissingExpression,
|
||||||
|
/// P0042
|
||||||
|
SwitchCaseExpressionInvalidStart,
|
||||||
// ================== Old errors to be thrown away! ==================
|
// ================== Old errors to be thrown away! ==================
|
||||||
/// Found an unexpected token while parsing an expression.
|
/// Found an unexpected token while parsing an expression.
|
||||||
ExpressionUnexpectedToken,
|
ExpressionUnexpectedToken,
|
||||||
@ -99,17 +117,6 @@ pub enum ParseErrorKind {
|
|||||||
TypeSpecClassMissingInnerType,
|
TypeSpecClassMissingInnerType,
|
||||||
TypeSpecClassMissingClosingAngle,
|
TypeSpecClassMissingClosingAngle,
|
||||||
BlockMissingSemicolonAfterStatement,
|
BlockMissingSemicolonAfterStatement,
|
||||||
/// `switch` has no body (missing matching braces).
|
|
||||||
SwitchMissingBody,
|
|
||||||
/// The first top-level item in a `switch` body is not a `case`.
|
|
||||||
SwitchTopLevelItemNotCase,
|
|
||||||
/// A `case` arm is missing the trailing `:`.
|
|
||||||
SwitchCaseMissingColon,
|
|
||||||
/// Found more than one `default` branch.
|
|
||||||
SwitchDuplicateDefault,
|
|
||||||
/// Found `case` arms after a `default` branch.
|
|
||||||
SwitchCasesAfterDefault,
|
|
||||||
SwitchMissingClosingBrace,
|
|
||||||
/// Unexpected end of input while parsing.
|
/// Unexpected end of input while parsing.
|
||||||
UnexpectedEndOfFile,
|
UnexpectedEndOfFile,
|
||||||
/// Token looked like a numeric literal but could not be parsed as one.
|
/// Token looked like a numeric literal but could not be parsed as one.
|
||||||
|
|||||||
@ -82,10 +82,10 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
/// This method never consumes the closing `}` and is only meant to be
|
/// This method never consumes the closing `}` and is only meant to be
|
||||||
/// called while parsing inside a block.
|
/// called while parsing inside a block.
|
||||||
///
|
///
|
||||||
/// On success, it appends exactly one statement. If neither a statement nor
|
/// On success, it appends exactly one statement and advances past at least
|
||||||
/// an expression statement can be recovered locally, it returns
|
/// one token. If statement cannot be recovered locally, it returns
|
||||||
/// an unreported error so the enclosing block parser can recover at
|
/// an unreported error so the enclosing block parser can recover
|
||||||
/// block level.
|
/// at block level.
|
||||||
pub(crate) fn parse_and_append_next_block_item(
|
pub(crate) fn parse_and_append_next_block_item(
|
||||||
&mut self,
|
&mut self,
|
||||||
statements: &mut StatementList<'src, 'arena>,
|
statements: &mut StatementList<'src, 'arena>,
|
||||||
@ -126,23 +126,30 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a non-statement starter as an expression statement inside
|
||||||
|
/// a block.
|
||||||
|
///
|
||||||
|
/// On success, returns an expression statement whose expression parser has
|
||||||
|
/// consumed at least one token. If expression parsing fails and recovery
|
||||||
|
/// cannot consume anything locally, returns the unreported error instead of
|
||||||
|
/// producing a zero-width fallback statement.
|
||||||
fn parse_expression_statement_in_block(
|
fn parse_expression_statement_in_block(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> ParseResult<'src, 'arena, StatementRef<'src, 'arena>> {
|
) -> ParseResult<'src, 'arena, StatementRef<'src, 'arena>> {
|
||||||
let expression_start_position = self.peek_position_or_eof();
|
let expression_start_position = self.peek_position_or_eof();
|
||||||
let expected_block_item_after_position = self.last_consumed_position_or_start();
|
let expected_block_item_after_position = self.last_consumed_position_or_start();
|
||||||
let expression_result = self.parse_expression_with_start_error(
|
let expression_result = self.parse_required_expression(
|
||||||
ParseErrorKind::BlockExpectedItem,
|
ParseErrorKind::BlockExpectedItem,
|
||||||
expected_block_item_after_position,
|
expected_block_item_after_position,
|
||||||
expected_block_item_after_position,
|
|
||||||
);
|
);
|
||||||
let expression = match expression_result {
|
let expression = match expression_result {
|
||||||
Ok(expression) => expression,
|
Ok(expression) => expression,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let expression_recovery_made_no_progress =
|
let expression_recovery_made_no_progress =
|
||||||
self.peek_position_or_eof() == expression_start_position;
|
self.peek_position_or_eof() == expression_start_position;
|
||||||
// Without progress, a fallback statement could leave
|
// Without progress, a fallback statement would violate this
|
||||||
// the block loop stuck.
|
// function's success contract and could leave the enclosing
|
||||||
|
// block loop stuck.
|
||||||
if expression_recovery_made_no_progress {
|
if expression_recovery_made_no_progress {
|
||||||
return self.recover_bad_block_item_start_as_error_statement(error);
|
return self.recover_bad_block_item_start_as_error_statement(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,28 @@
|
|||||||
//! Parser for `for` loop expressions in Fermented UnrealScript.
|
//! Parser for `for` loop expressions in Fermented UnrealScript.
|
||||||
|
//!
|
||||||
|
//! ## Disambiguation of `for` as loop vs expression
|
||||||
|
//!
|
||||||
|
//! Unlike other control-flow keywords, `for` is disambiguated from functions
|
||||||
|
//! and variables with the same name. This is done syntactically in
|
||||||
|
//! [`Parser::peek_for_loop_header_left_parenthesis_position`]: a `for` token
|
||||||
|
//! followed by a `(` whose contents contain a top-level `;` is unambiguously
|
||||||
|
//! a loop header.
|
||||||
|
//!
|
||||||
|
//! This rule is lightweight, local, and robust, and mirrors the fixed grammar
|
||||||
|
//! `for (init; condition; step)` without requiring name resolution.
|
||||||
|
//!
|
||||||
|
//! ### Why this is not done for `if` / `while` / `do`
|
||||||
|
//!
|
||||||
|
//! There is no similarly reliable way to discriminate `if`, `while`, or related
|
||||||
|
//! keywords at this stage of parsing: their parenthesized forms are
|
||||||
|
//! indistinguishable from single-argument function calls.
|
||||||
|
//!
|
||||||
|
//! Supporting these keywords as identifiers would complicate parsing
|
||||||
|
//! disproportionately and we always treat them as openers for conditional and
|
||||||
|
//! loop expressions. This matches common `UnrealScript` usage and
|
||||||
|
//! intentionally drops support for moronic design choices where such names were
|
||||||
|
//! reused as variables or functions (like what author did by declaring
|
||||||
|
//! a `For` function in Acedia).
|
||||||
|
|
||||||
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
|
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
|
||||||
use crate::lexer::{self, Token, TokenPosition};
|
use crate::lexer::{self, Token, TokenPosition};
|
||||||
@ -115,7 +139,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
&& next_token != terminator_token
|
&& next_token != terminator_token
|
||||||
{
|
{
|
||||||
Some(
|
Some(
|
||||||
self.parse_expression_with_start_error(
|
self.parse_required_expression_with_context(
|
||||||
invalid_start_error_kind,
|
invalid_start_error_kind,
|
||||||
for_keyword_position,
|
for_keyword_position,
|
||||||
component_start_anchor_position,
|
component_start_anchor_position,
|
||||||
|
|||||||
@ -18,9 +18,9 @@
|
|||||||
//! legacy cut-off rule:
|
//! legacy cut-off rule:
|
||||||
//!
|
//!
|
||||||
//! If the condition begins with a parenthesized expression, and the token after
|
//! If the condition begins with a parenthesized expression, and the token after
|
||||||
//! the matching `)` is identifier-like, the parenthesized expression is treated
|
//! the matching `)` is identifier-like, the parser treats only the
|
||||||
//! as the whole condition. The following identifier-like token is left for the
|
//! parenthesized expression as the condition. The following identifier-like
|
||||||
//! branch body.
|
//! token is left for the branch body.
|
||||||
//!
|
//!
|
||||||
//! This prevents the parser from accidentally consuming the following
|
//! This prevents the parser from accidentally consuming the following
|
||||||
//! statement/body as part of the condition in older code such as:
|
//! statement/body as part of the condition in older code such as:
|
||||||
@ -30,8 +30,8 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Without the legacy cut-off, a permissive expression parser could interpret
|
//! Without the legacy cut-off, a permissive expression parser could interpret
|
||||||
//! `Cross` as a continuation of the condition in dialects where identifier-like
|
//! the identifier-like body opener, such as `Cross`, as an operator-like
|
||||||
//! tokens may participate in operator syntax.
|
//! continuation of the parenthesized condition.
|
||||||
//!
|
//!
|
||||||
//! Operator tokens such as `*`, `+`, `<`, `==`, etc. do not trigger this
|
//! Operator tokens such as `*`, `+`, `<`, `==`, etc. do not trigger this
|
||||||
//! legacy cut-off. They allow the normal expression parser to continue the
|
//! legacy cut-off. They allow the normal expression parser to continue the
|
||||||
@ -41,43 +41,6 @@
|
|||||||
//! a custom/named operator, the parser prefers the legacy interpretation and
|
//! a custom/named operator, the parser prefers the legacy interpretation and
|
||||||
//! ends the condition at the closing `)`. Write the condition with additional
|
//! ends the condition at the closing `)`. Write the condition with additional
|
||||||
//! parentheses or use an unambiguous operator form.
|
//! parentheses or use an unambiguous operator form.
|
||||||
//!
|
|
||||||
//! ## Disambiguation of `for` as loop vs expression
|
|
||||||
//!
|
|
||||||
//! Unlike other control-flow keywords, `for` is disambiguated from functions
|
|
||||||
//! and variables with the same name. This is done syntactically in
|
|
||||||
//! [`Parser::is_for_loop_header_ahead`]: a `for` token followed by
|
|
||||||
//! a `(` whose contents contain a top-level `;` is unambiguously a loop header.
|
|
||||||
//!
|
|
||||||
//! This rule is lightweight, local, and robust, and mirrors the fixed grammar
|
|
||||||
//! `for (init; condition; step)` without requiring name resolution.
|
|
||||||
//!
|
|
||||||
//! ### Why this is not done for `if` / `while` / `do`
|
|
||||||
//!
|
|
||||||
//! There is no similarly reliable way to discriminate `if`, `while`, or related
|
|
||||||
//! keywords at this stage of parsing: their parenthesized forms are
|
|
||||||
//! indistinguishable from single argument function calls.
|
|
||||||
//!
|
|
||||||
//! Supporting these keywords as identifiers would complicate parsing
|
|
||||||
//! disproportionately and we always treat them as openers for conditional and
|
|
||||||
//! loop expressions. This matches common `UnrealScript` usage and
|
|
||||||
//! intentionally drops support for moronic design choices where such names were
|
|
||||||
//! reused as variables or functions (like what author did by declaring
|
|
||||||
//! a `For` function in Acedia).
|
|
||||||
//!
|
|
||||||
//! ### But what about `switch`?
|
|
||||||
//!
|
|
||||||
//! `switch` is handled separately because, in existing `UnrealScript` code,
|
|
||||||
//! it may appear either as a keyword-led construct or as an identifier.
|
|
||||||
//!
|
|
||||||
//! Its disambiguation rule is simpler than for `for`: if the next token is
|
|
||||||
//! `(`, `switch` is parsed as a `switch` expression; otherwise it remains
|
|
||||||
//! available as an identifier.
|
|
||||||
//!
|
|
||||||
//! This rule is local and purely syntactic, matching the behavior expected by
|
|
||||||
//! the existing codebase we support. The actual parsing of `switch` expressions
|
|
||||||
//! lives in a separate module because the construct itself is more involved
|
|
||||||
//! than the control-flow forms handled here.
|
|
||||||
|
|
||||||
use crate::ast::{BranchBody, Expression, ExpressionRef};
|
use crate::ast::{BranchBody, Expression, ExpressionRef};
|
||||||
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
||||||
@ -210,7 +173,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
branch_owner_keyword_position: TokenPosition,
|
branch_owner_keyword_position: TokenPosition,
|
||||||
) -> BranchBody<'src, 'arena> {
|
) -> BranchBody<'src, 'arena> {
|
||||||
let branch_expression = self
|
let branch_expression = self
|
||||||
.parse_expression_with_start_error(
|
.parse_required_expression_with_context(
|
||||||
ParseErrorKind::ControlFlowBodyExpected,
|
ParseErrorKind::ControlFlowBodyExpected,
|
||||||
branch_owner_keyword_position,
|
branch_owner_keyword_position,
|
||||||
self.last_consumed_position_or_start(),
|
self.last_consumed_position_or_start(),
|
||||||
@ -415,10 +378,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
None | Some(Token::Semicolon) => (None, TokenSpan::new(return_keyword_position)),
|
None | Some(Token::Semicolon) => (None, TokenSpan::new(return_keyword_position)),
|
||||||
_ => {
|
_ => {
|
||||||
let return_value = self
|
let return_value = self
|
||||||
.parse_expression_with_start_error(
|
.parse_required_expression(
|
||||||
ParseErrorKind::ReturnValueInvalidStart,
|
ParseErrorKind::ReturnValueInvalidStart,
|
||||||
return_keyword_position,
|
return_keyword_position,
|
||||||
return_keyword_position,
|
|
||||||
)
|
)
|
||||||
.unwrap_or_fallback(self);
|
.unwrap_or_fallback(self);
|
||||||
let span = TokenSpan::range(return_keyword_position, return_value.span().end);
|
let span = TokenSpan::range(return_keyword_position, return_value.span().end);
|
||||||
@ -444,10 +406,9 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
None | Some(Token::Semicolon) => (None, TokenSpan::new(break_keyword_position)),
|
None | Some(Token::Semicolon) => (None, TokenSpan::new(break_keyword_position)),
|
||||||
_ => {
|
_ => {
|
||||||
let break_value = self
|
let break_value = self
|
||||||
.parse_expression_with_start_error(
|
.parse_required_expression(
|
||||||
ParseErrorKind::BreakValueInvalidStart,
|
ParseErrorKind::BreakValueInvalidStart,
|
||||||
break_keyword_position,
|
break_keyword_position,
|
||||||
break_keyword_position,
|
|
||||||
)
|
)
|
||||||
.unwrap_or_fallback(self);
|
.unwrap_or_fallback(self);
|
||||||
let span = TokenSpan::range(break_keyword_position, break_value.span().end);
|
let span = TokenSpan::range(break_keyword_position, break_value.span().end);
|
||||||
|
|||||||
@ -86,15 +86,35 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
/// `expression_context_position` identifies the local syntactic anchor after
|
/// `expression_context_position` identifies the local syntactic anchor after
|
||||||
/// which the expression was expected. It is attached to the diagnostic with
|
/// which the expression was expected. It is attached to the diagnostic with
|
||||||
/// the [`diagnostic_labels::EXPRESSION_EXPECTED_AFTER`] label.
|
/// the [`diagnostic_labels::EXPRESSION_EXPECTED_AFTER`] label.
|
||||||
pub(super) fn parse_expression_with_start_error(
|
pub(super) fn parse_required_expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
bad_start_error_kind: ParseErrorKind,
|
bad_start_error_kind: ParseErrorKind,
|
||||||
required_by_position: crate::lexer::TokenPosition,
|
required_by_position: TokenPosition,
|
||||||
expression_context_position: crate::lexer::TokenPosition,
|
) -> ParseExpressionResult<'src, 'arena> {
|
||||||
|
if self.next_token_definitely_cannot_start_expression() {
|
||||||
|
let error_position = self.peek_position_or_eof();
|
||||||
|
|
||||||
|
return Err(self
|
||||||
|
.make_error_at(bad_start_error_kind, error_position)
|
||||||
|
.sync_error_until(self, crate::parser::SyncLevel::ExpressionStart)
|
||||||
|
.blame_token(error_position)
|
||||||
|
.related_token(
|
||||||
|
diagnostic_labels::EXPRESSION_REQUIRED_BY,
|
||||||
|
required_by_position,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_required_expression_with_context(
|
||||||
|
&mut self,
|
||||||
|
bad_start_error_kind: ParseErrorKind,
|
||||||
|
required_by_position: TokenPosition,
|
||||||
|
expression_context_position: TokenPosition,
|
||||||
) -> ParseExpressionResult<'src, 'arena> {
|
) -> ParseExpressionResult<'src, 'arena> {
|
||||||
if self.next_token_definitely_cannot_start_expression() {
|
if self.next_token_definitely_cannot_start_expression() {
|
||||||
let error_position = self.peek_position_or_eof();
|
let error_position = self.peek_position_or_eof();
|
||||||
//self.advance();
|
|
||||||
|
|
||||||
return Err(self
|
return Err(self
|
||||||
.make_error_at(bad_start_error_kind, error_position)
|
.make_error_at(bad_start_error_kind, error_position)
|
||||||
@ -109,6 +129,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
expression_context_position,
|
expression_context_position,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
|
self.parse_expression_with_min_precedence_rank(PrecedenceRank::LOOSEST)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,9 +180,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
// Avoid advancing over an obviously wrong token;
|
// Avoid advancing over an obviously wrong token;
|
||||||
// this prevents error cases like `new(Outer, Name, 7 +) SomeClass`.
|
// this prevents error cases like `new(Outer, Name, 7 +) SomeClass`.
|
||||||
if token.is_definitely_not_expression_start() {
|
if token.is_definitely_not_expression_start() {
|
||||||
return Err(
|
return Err(self.make_error_at(ParseErrorKind::ExpressionExpected, token_position));
|
||||||
self.make_error_at(ParseErrorKind::ExpressionExpected, token_position)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
self.advance();
|
self.advance();
|
||||||
if let Ok(operator) = ast::PrefixOperator::try_from(token) {
|
if let Ok(operator) = ast::PrefixOperator::try_from(token) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! This module implements parsing of primary expressions via
|
//! This module implements parsing of primary expressions via
|
||||||
//! [`Parser::parse_primary_from_current_token`] and its helper
|
//! [`Parser::parse_primary_from_current_token`] and its helper
|
||||||
//! [`Parser::parse_keyword_primary`].
|
//! [`Parser::try_parse_keyword_primary`].
|
||||||
//!
|
//!
|
||||||
//! ## What is a "primary expression" here?
|
//! ## What is a "primary expression" here?
|
||||||
//!
|
//!
|
||||||
@ -25,6 +25,34 @@
|
|||||||
//! So "primary" here does not mean "smallest atomic expression".
|
//! So "primary" here does not mean "smallest atomic expression".
|
||||||
//! It means "an expression form that does not need a left-hand side
|
//! It means "an expression form that does not need a left-hand side
|
||||||
//! in order to be parsed".
|
//! in order to be parsed".
|
||||||
|
//!
|
||||||
|
//! ## Keyword-led primaries and identifier fallback
|
||||||
|
//!
|
||||||
|
//! Some lexer keywords are always parsed as keyword-led primary expressions
|
||||||
|
//! in expression position: `if`, `while`, `do`, `foreach`, `return`, `break`,
|
||||||
|
//! `continue`, `new`, `true`, `false`, and `none`.
|
||||||
|
//!
|
||||||
|
//! Other keywords are accepted as keyword-led forms only when the following
|
||||||
|
//! tokens commit to that syntax. Otherwise they remain available as
|
||||||
|
//! identifier-like primaries.
|
||||||
|
//!
|
||||||
|
//! - `for` is parsed as a loop only when followed by a parenthesized header
|
||||||
|
//! containing a top-level `;`, matching `for (init; condition; step)`.
|
||||||
|
//! - `switch` is parsed as a switch expression only when followed by `(`.
|
||||||
|
//! - `goto` is parsed as a label jump only when it is not followed by `(`.
|
||||||
|
//! - `class` is parsed as a class type expression only when followed by `<`.
|
||||||
|
//!
|
||||||
|
//! These rules are local and syntactic. They avoid name resolution while still
|
||||||
|
//! supporting existing legacy code that uses some keywords as ordinary names.
|
||||||
|
//!
|
||||||
|
//! ### Why is `switch` handled differently?
|
||||||
|
//!
|
||||||
|
//! `switch` is handled differently because, in existing `UnrealScript` code,
|
||||||
|
//! it may appear either as a keyword-led construct or as an identifier.
|
||||||
|
//!
|
||||||
|
//! Its disambiguation rule is simpler than for `for`: if the next token is
|
||||||
|
//! `(`, `switch` is parsed as a `switch` expression; otherwise it remains
|
||||||
|
//! available as an identifier.
|
||||||
|
|
||||||
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
|
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
|
||||||
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ use crate::parser::{ParseErrorKind, Parser, ResultRecoveryExt, SyncLevel};
|
|||||||
|
|
||||||
/// Determines how parsing of the class specifier proceeds after the optional
|
/// Determines how parsing of the class specifier proceeds after the optional
|
||||||
/// `new(...)` argument list has been parsed or recovered.
|
/// `new(...)` argument list has been parsed or recovered.
|
||||||
|
#[must_use]
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
enum NewClassSpecifierParseAction {
|
enum NewClassSpecifierParseAction {
|
||||||
Parse,
|
Parse,
|
||||||
|
|||||||
@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
|||||||
left_bracket_position: TokenPosition,
|
left_bracket_position: TokenPosition,
|
||||||
) -> ParseExpressionResult<'src, 'arena> {
|
) -> ParseExpressionResult<'src, 'arena> {
|
||||||
let index_expression = self
|
let index_expression = self
|
||||||
.parse_expression_with_start_error(
|
.parse_required_expression_with_context(
|
||||||
ParseErrorKind::IndexMissingExpression,
|
ParseErrorKind::IndexMissingExpression,
|
||||||
left_hand_side.span().end,
|
left_hand_side.span().end,
|
||||||
left_bracket_position,
|
left_bracket_position,
|
||||||
|
|||||||
@ -1,176 +1,508 @@
|
|||||||
//! Switch parsing for Fermented `UnrealScript`.
|
//! Parsing for `switch (...) { ... }` expressions in Fermented UnrealScript.
|
||||||
//!
|
//!
|
||||||
//! Provides routines for parsing `switch (...) { ... }` expressions.
|
//! Dispatch into this module happens only after `primary.rs` has committed to
|
||||||
use crate::arena::ArenaVec;
|
//! keyword-led `switch` syntax. That commitment is purely syntactic: `switch`
|
||||||
use crate::ast::{ExpressionRef, StatementRef};
|
//! followed by `(` is treated as a switch expression; otherwise `switch` may
|
||||||
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
//! still be parsed as an identifier-like primary.
|
||||||
use crate::parser::{ParseErrorKind, ResultRecoveryExt};
|
//!
|
||||||
|
//! This module owns parsing of the selector, body braces, `case` labels,
|
||||||
|
//! `default`, duplicate-default diagnostics, cases-after-default diagnostics,
|
||||||
|
//! and recovery for invalid top-level switch items.
|
||||||
|
|
||||||
impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
use crate::arena::ArenaVec;
|
||||||
/// Parses a `switch` expression after the `switch` keyword has been
|
use crate::ast::{self, ExpressionRef, StatementList, StatementRef, SwitchCaseRef};
|
||||||
/// consumed.
|
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
||||||
|
use crate::parser::{
|
||||||
|
ParseError, ParseErrorKind, ParseResult, Parser, ResultRecoveryExt, SyncLevel,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parser structure:
|
||||||
|
|
||||||
|
switch body = everything between `{` and `}`
|
||||||
|
switch section = one labeled part: `case ...:` body or `default:` body
|
||||||
|
case labels = stacked `case <expr>:` labels
|
||||||
|
section body = statements until `case`, `default`, or `}`
|
||||||
|
preamble = invalid statements before the first section label
|
||||||
|
|
||||||
|
parse_switch_tail
|
||||||
|
parse_switch_header_tail
|
||||||
|
parse_switch_sections_tail
|
||||||
|
parse_case_section_into_state
|
||||||
|
parse_case_labels
|
||||||
|
expect_case_label_colon
|
||||||
|
parse_switch_section_body
|
||||||
|
parse_default_section_into_state
|
||||||
|
parse_switch_section_body
|
||||||
|
parse_invalid_switch_preamble
|
||||||
|
parse_switch_section_body
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SwitchParseState<'src, 'arena> {
|
||||||
|
switch_keyword_position: TokenPosition,
|
||||||
|
left_brace_position: TokenPosition,
|
||||||
|
|
||||||
|
selector: ExpressionRef<'src, 'arena>,
|
||||||
|
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
|
||||||
|
default_arm: Option<StatementList<'src, 'arena>>,
|
||||||
|
|
||||||
|
// Retained until the full switch is parsed so diagnostics can report all
|
||||||
|
// duplicate `default`s and all `case`s that follow the first `default`.
|
||||||
|
default_keyword_positions: Vec<TokenPosition>,
|
||||||
|
case_keyword_positions_after_default: Vec<TokenPosition>,
|
||||||
|
|
||||||
|
span: TokenSpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src, 'arena> SwitchParseState<'src, 'arena> {
|
||||||
|
#[must_use]
|
||||||
|
fn new(
|
||||||
|
switch_keyword_position: TokenPosition,
|
||||||
|
left_brace_position: TokenPosition,
|
||||||
|
selector: ExpressionRef<'src, 'arena>,
|
||||||
|
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
switch_keyword_position,
|
||||||
|
left_brace_position,
|
||||||
|
selector,
|
||||||
|
cases,
|
||||||
|
default_arm: None,
|
||||||
|
default_keyword_positions: Vec::new(),
|
||||||
|
case_keyword_positions_after_default: Vec::new(),
|
||||||
|
span: TokenSpan::new(switch_keyword_position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn has_default(&self) -> bool {
|
||||||
|
!self.default_keyword_positions.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn first_default_keyword_position(&self) -> Option<TokenPosition> {
|
||||||
|
self.default_keyword_positions.first().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn diagnostic_context(&self) -> SwitchDiagnosticContext {
|
||||||
|
SwitchDiagnosticContext {
|
||||||
|
switch_keyword_position: self.switch_keyword_position,
|
||||||
|
selector_span: *self.selector.span(),
|
||||||
|
left_brace_position: self.left_brace_position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carries source locations reused by switch diagnostics.
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
|
struct SwitchDiagnosticContext {
|
||||||
|
switch_keyword_position: TokenPosition,
|
||||||
|
selector_span: TokenSpan,
|
||||||
|
left_brace_position: TokenPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchDiagnosticContext {
|
||||||
|
#[must_use]
|
||||||
|
fn attach_to_error(self, error: ParseError) -> ParseError {
|
||||||
|
error
|
||||||
|
.related_token("switch_keyword", self.switch_keyword_position)
|
||||||
|
.related("selector", self.selector_span)
|
||||||
|
.related_token("left_brace", self.left_brace_position)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn attach_to_result<'src, 'arena, T>(
|
||||||
|
self,
|
||||||
|
result: ParseResult<'src, 'arena, T>,
|
||||||
|
) -> ParseResult<'src, 'arena, T> {
|
||||||
|
result.map_err(|error| self.attach_to_error(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
|
enum SwitchSectionBodyExit {
|
||||||
|
AtSectionBoundary,
|
||||||
|
RecoveredAtSwitchBoundary,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
|
enum SwitchSectionsExit {
|
||||||
|
ClosedByRightBrace,
|
||||||
|
ClosedByRecovery,
|
||||||
|
EndOfFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src, 'arena> Parser<'src, 'arena> {
|
||||||
|
/// Parses a `switch` expression after consuming the `switch` keyword.
|
||||||
///
|
///
|
||||||
/// Returns an [`crate::ast::Expression::Switch`] whose span covers the
|
/// If the switch is closed normally, returns an [`ast::Expression::Switch`]
|
||||||
/// entire construct, from `switch_start_position` to the closing `}`.
|
/// whose span covers the construct from `switch_start_position` through the
|
||||||
|
/// closing `}`.
|
||||||
///
|
///
|
||||||
/// Only one `default` arm is recorded. Duplicate defaults and `case` arms
|
/// On errors, reports diagnostics and returns a best-effort switch node.
|
||||||
/// after a `default` are reported as errors.
|
|
||||||
///
|
|
||||||
/// On premature end-of-file, reports an error and returns a best-effort
|
|
||||||
/// switch node.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn parse_switch_tail(
|
pub(crate) fn parse_switch_tail(
|
||||||
&mut self,
|
&mut self,
|
||||||
switch_start_position: TokenPosition,
|
switch_start_position: TokenPosition,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
let selector = self.parse_expression();
|
let mut state = match self.parse_switch_header_tail(switch_start_position) {
|
||||||
let mut cases = self.arena.vec();
|
Ok(state) => state,
|
||||||
let mut default_arm = None;
|
Err(switch_node) => return switch_node,
|
||||||
let mut span = TokenSpan::new(switch_start_position);
|
};
|
||||||
if self
|
match self.parse_switch_sections_tail(&mut state) {
|
||||||
.expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
|
SwitchSectionsExit::ClosedByRightBrace | SwitchSectionsExit::ClosedByRecovery => {
|
||||||
.report_error(self)
|
self.report_delayed_switch_errors(&state);
|
||||||
{
|
self.alloc_switch_node_from_state(state)
|
||||||
return self.alloc_switch_node(selector, cases, default_arm, span);
|
}
|
||||||
|
SwitchSectionsExit::EndOfFile => {
|
||||||
|
let eof_position = self.peek_position_or_eof();
|
||||||
|
state
|
||||||
|
.diagnostic_context()
|
||||||
|
.attach_to_error(
|
||||||
|
self.make_error_at(ParseErrorKind::SwitchMissingClosingBrace, eof_position)
|
||||||
|
.sync_error_at_matching_delimiter(self, state.left_brace_position),
|
||||||
|
)
|
||||||
|
.report(self);
|
||||||
|
|
||||||
|
state.span.extend_to(self.last_consumed_position_or_start());
|
||||||
|
self.report_delayed_switch_errors(&state);
|
||||||
|
self.alloc_switch_node_from_state(state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_switch_header_tail(
|
||||||
|
&mut self,
|
||||||
|
switch_start_position: TokenPosition,
|
||||||
|
) -> Result<SwitchParseState<'src, 'arena>, ExpressionRef<'src, 'arena>> {
|
||||||
|
// The caller has already accepted `switch` as expression syntax,
|
||||||
|
// so selector parsing can rely on normal expression recovery instead of
|
||||||
|
// revalidating the legacy `switch (` disambiguation.
|
||||||
|
let selector = self.parse_expression();
|
||||||
|
let span = TokenSpan::new(switch_start_position).extended(selector.span().end);
|
||||||
|
let Some(left_brace_position) = self
|
||||||
|
.expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
|
||||||
|
.related("selector", *selector.span())
|
||||||
|
.related_token("switch_keyword", switch_start_position)
|
||||||
|
.ok_or_report(self)
|
||||||
|
else {
|
||||||
|
return Err(self.alloc_switch_node(selector, self.arena.vec(), None, span));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SwitchParseState::new(
|
||||||
|
switch_start_position,
|
||||||
|
left_brace_position,
|
||||||
|
selector,
|
||||||
|
self.arena.vec(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_switch_sections_tail(
|
||||||
|
&mut self,
|
||||||
|
state: &mut SwitchParseState<'src, 'arena>,
|
||||||
|
) -> SwitchSectionsExit {
|
||||||
while let Some((token, token_position)) = self.peek_token_and_position() {
|
while let Some((token, token_position)) = self.peek_token_and_position() {
|
||||||
match token {
|
let body_exit = match token {
|
||||||
Token::RightBrace => {
|
Token::RightBrace => {
|
||||||
self.advance(); // '}'
|
self.advance(); // '}'
|
||||||
span.extend_to(token_position);
|
state.span.extend_to(token_position);
|
||||||
return self.alloc_switch_node(selector, cases, default_arm, span);
|
return SwitchSectionsExit::ClosedByRightBrace;
|
||||||
}
|
}
|
||||||
Token::Keyword(Keyword::Case) => {
|
Token::Keyword(Keyword::Case) => {
|
||||||
if default_arm.is_some() {
|
self.parse_case_section_into_state(state, token_position)
|
||||||
self.report_error_here(ParseErrorKind::SwitchCasesAfterDefault);
|
|
||||||
}
|
|
||||||
let case_node = self.parse_switch_case_group(token_position);
|
|
||||||
cases.push(case_node);
|
|
||||||
}
|
}
|
||||||
Token::Keyword(Keyword::Default) => {
|
Token::Keyword(Keyword::Default) => {
|
||||||
if default_arm.is_some() {
|
self.parse_default_section_into_state(state, token_position)
|
||||||
self.report_error_here(ParseErrorKind::SwitchDuplicateDefault);
|
|
||||||
}
|
|
||||||
// Duplicate `default` is still parsed so that diagnostics
|
|
||||||
// in its body can be reported.
|
|
||||||
self.parse_switch_default_arm(
|
|
||||||
token_position,
|
|
||||||
default_arm.get_or_insert_with(|| self.arena.vec()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Items before the first arm declaration are not allowed, but
|
// Invalid switch-level items are parsed before being discarded
|
||||||
// are parsed for basic diagnostics and simplicity.
|
// so ordinary statement diagnostics still run.
|
||||||
_ => self.parse_switch_preamble_items(token_position),
|
_ => self.parse_invalid_switch_preamble(state.diagnostic_context(), token_position),
|
||||||
|
};
|
||||||
|
if body_exit == SwitchSectionBodyExit::RecoveredAtSwitchBoundary {
|
||||||
|
state.span.extend_to(self.last_consumed_position_or_start());
|
||||||
|
return SwitchSectionsExit::ClosedByRecovery;
|
||||||
}
|
}
|
||||||
|
// Guard against parser bugs that would otherwise leave
|
||||||
|
// block parsing stuck on the same token.
|
||||||
self.ensure_forward_progress(token_position);
|
self.ensure_forward_progress(token_position);
|
||||||
}
|
}
|
||||||
self.report_error_here(ParseErrorKind::SwitchMissingClosingBrace);
|
SwitchSectionsExit::EndOfFile
|
||||||
// This can only be `None` in the pathological case of
|
|
||||||
// an empty token stream
|
|
||||||
span.extend_to(
|
|
||||||
self.last_consumed_position()
|
|
||||||
.unwrap_or(switch_start_position),
|
|
||||||
);
|
|
||||||
self.alloc_switch_node(selector, cases, default_arm, span)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a stacked `case` group and its body:
|
/// Parses a `case` section and appends it to `state`.
|
||||||
/// `case <expr>: (case <expr>:)* <arm-body>`.
|
|
||||||
///
|
///
|
||||||
/// Returns the allocated [`crate::ast::CaseRef`] node.
|
/// A section may have stacked `case <expr>:` labels and contains statements
|
||||||
|
/// until the next section boundary.
|
||||||
///
|
///
|
||||||
/// The returned node span covers the entire group, from
|
/// Returns the boundary that stopped body parsing.
|
||||||
/// `first_case_position` to the end of the arm body, or to the end of the
|
fn parse_case_section_into_state(
|
||||||
/// last label if the body is empty.
|
|
||||||
#[must_use]
|
|
||||||
fn parse_switch_case_group(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
|
state: &mut SwitchParseState<'src, 'arena>,
|
||||||
first_case_position: TokenPosition,
|
first_case_position: TokenPosition,
|
||||||
) -> crate::ast::SwitchCaseRef<'src, 'arena> {
|
) -> SwitchSectionBodyExit {
|
||||||
let mut labels = self.arena.vec();
|
if state.has_default() {
|
||||||
while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
|
state
|
||||||
self.advance(); // 'case'
|
.case_keyword_positions_after_default
|
||||||
labels.push(self.parse_expression());
|
.push(first_case_position);
|
||||||
|
|
||||||
// `:` is required after each case label; missing `:` is recovered
|
|
||||||
// at statement sync level.
|
|
||||||
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
|
|
||||||
.widen_error_span_from(case_position)
|
|
||||||
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
|
|
||||||
.report_error(self);
|
|
||||||
}
|
}
|
||||||
let mut body = self.arena.vec();
|
let switch_context = state.diagnostic_context();
|
||||||
self.parse_switch_arm_body(&mut body);
|
let labels = self.parse_case_labels(switch_context);
|
||||||
let case_span = compute_case_span(first_case_position, &labels, &body);
|
let mut statements = self.arena.vec();
|
||||||
self.arena
|
let body_exit =
|
||||||
.alloc_node(crate::ast::SwitchCase { labels, body }, case_span)
|
self.parse_switch_section_body(switch_context.left_brace_position, &mut statements);
|
||||||
|
let case_span = compute_switch_case_span(first_case_position, &labels, &statements);
|
||||||
|
let case_node = self.arena.alloc_node(
|
||||||
|
ast::SwitchCase {
|
||||||
|
labels,
|
||||||
|
body: statements,
|
||||||
|
},
|
||||||
|
case_span,
|
||||||
|
);
|
||||||
|
state.cases.push(case_node);
|
||||||
|
|
||||||
|
body_exit
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a `default:` arm and appends its statements to `statements`.
|
/// Parses a `default:` section into `state`.
|
||||||
fn parse_switch_default_arm(
|
///
|
||||||
|
/// Duplicate `default` sections contribute statements to the first one so
|
||||||
|
/// recovery preserves their bodies while diagnostics are delayed.
|
||||||
|
///
|
||||||
|
/// Returns the boundary that stopped body parsing.
|
||||||
|
fn parse_default_section_into_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
state: &mut SwitchParseState<'src, 'arena>,
|
||||||
default_position: TokenPosition,
|
default_position: TokenPosition,
|
||||||
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
|
) -> SwitchSectionBodyExit {
|
||||||
) {
|
|
||||||
self.advance(); // 'default'
|
self.advance(); // 'default'
|
||||||
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
|
state.default_keyword_positions.push(default_position);
|
||||||
.widen_error_span_from(default_position)
|
let switch_context = state.diagnostic_context();
|
||||||
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
|
let default_arm = state.default_arm.get_or_insert_with(|| self.arena.vec());
|
||||||
|
switch_context
|
||||||
|
.attach_to_result(
|
||||||
|
self.expect(Token::Colon, ParseErrorKind::SwitchDefaultMissingColon)
|
||||||
|
.widen_error_span_from(default_position)
|
||||||
|
.related_token("default_keyword", default_position),
|
||||||
|
)
|
||||||
|
.sync_error_until(self, SyncLevel::StatementStart)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
self.parse_switch_arm_body(statements);
|
self.parse_switch_section_body(switch_context.left_brace_position, default_arm)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses statements of a single switch arm body.
|
/// Parses invalid switch-level items up to the next section boundary.
|
||||||
fn parse_switch_arm_body(
|
///
|
||||||
|
/// Parsed statements are discarded after diagnostics; they are not part of
|
||||||
|
/// any switch arm.
|
||||||
|
fn parse_invalid_switch_preamble(
|
||||||
&mut self,
|
&mut self,
|
||||||
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
|
switch_context: SwitchDiagnosticContext,
|
||||||
) {
|
preamble_start_position: TokenPosition,
|
||||||
while let Some((token, token_position)) = self.peek_token_and_position() {
|
) -> SwitchSectionBodyExit
|
||||||
match token {
|
|
||||||
Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => break,
|
|
||||||
_ => {
|
|
||||||
self.parse_and_append_next_block_item(statements);
|
|
||||||
self.ensure_forward_progress(token_position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses items that appear before any `case` or `default` arm declaration.
|
|
||||||
///
|
|
||||||
/// Such items are not allowed, but they are parsed to produce diagnostics
|
|
||||||
/// and maintain forward progress.
|
|
||||||
///
|
|
||||||
/// Parsed statements are discarded; only error reporting is preserved.
|
|
||||||
///
|
|
||||||
/// Parsing stops at a boundary token or end-of-file.
|
|
||||||
/// Boundary tokens: `case`, `default`, `}`.
|
|
||||||
fn parse_switch_preamble_items(&mut self, preamble_start_position: TokenPosition)
|
|
||||||
where
|
where
|
||||||
'src: 'arena,
|
'src: 'arena,
|
||||||
{
|
{
|
||||||
// Discard parsed statements into a sink vector.
|
// Build the statements only to reuse normal statement diagnostics and
|
||||||
// This is a bit "hacky", but I don't want to adapt code to skip
|
// recovery; switch-level items cannot be represented in the switch AST.
|
||||||
// production of AST nodes just to report errors in
|
let mut discarded_statements = self.arena.vec();
|
||||||
// one problematic case.
|
let body_exit = self.parse_switch_section_body(
|
||||||
let mut sink = self.arena.vec();
|
switch_context.left_brace_position,
|
||||||
self.parse_switch_arm_body(&mut sink);
|
&mut discarded_statements,
|
||||||
self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase)
|
);
|
||||||
.widen_error_span_from(preamble_start_position)
|
let preamble_span = TokenSpan::range(
|
||||||
|
preamble_start_position,
|
||||||
|
self.last_consumed_position_or_start(),
|
||||||
|
);
|
||||||
|
let error = switch_context.attach_to_error(
|
||||||
|
self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase)
|
||||||
|
.widen_error_span_from(preamble_start_position)
|
||||||
|
.blame(preamble_span),
|
||||||
|
);
|
||||||
|
if discarded_statements.len() > 1 {
|
||||||
|
error.related("multiple_items", preamble_span)
|
||||||
|
} else {
|
||||||
|
error.related("single_item", preamble_span)
|
||||||
|
}
|
||||||
|
.report(self);
|
||||||
|
body_exit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_case_labels(
|
||||||
|
&mut self,
|
||||||
|
switch_context: SwitchDiagnosticContext,
|
||||||
|
) -> ArenaVec<'arena, ExpressionRef<'src, 'arena>> {
|
||||||
|
let mut labels = self.arena.vec();
|
||||||
|
while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
|
||||||
|
self.advance(); // 'case'
|
||||||
|
let mut case_expression_span = None;
|
||||||
|
let mut should_expect_colon = true;
|
||||||
|
if let Some((Token::Colon, colon_position)) = self.peek_token_and_position() {
|
||||||
|
switch_context
|
||||||
|
.attach_to_error(
|
||||||
|
self.make_error_at(
|
||||||
|
ParseErrorKind::SwitchCaseMissingExpression,
|
||||||
|
colon_position,
|
||||||
|
)
|
||||||
|
.blame_token(colon_position)
|
||||||
|
.related_token("case_keyword", case_position),
|
||||||
|
)
|
||||||
|
.report(self);
|
||||||
|
} else {
|
||||||
|
// Recover only to the label delimiter here;
|
||||||
|
// `expect_case_label_colon` will consume it and avoid
|
||||||
|
// a duplicate missing-colon diagnostic.
|
||||||
|
should_expect_colon = !self.next_token_definitely_cannot_start_expression();
|
||||||
|
let expression = switch_context
|
||||||
|
.attach_to_result(self.parse_required_expression(
|
||||||
|
ParseErrorKind::SwitchCaseExpressionInvalidStart,
|
||||||
|
case_position,
|
||||||
|
))
|
||||||
|
.related_token("case_keyword", case_position)
|
||||||
|
.sync_error_at(self, SyncLevel::ColonDelimiter)
|
||||||
|
.unwrap_or_fallback(self);
|
||||||
|
case_expression_span = Some(*expression.span());
|
||||||
|
labels.push(expression);
|
||||||
|
}
|
||||||
|
// Expression recovery may still leave a valid colon to consume
|
||||||
|
if self.peek_token() == Some(Token::Colon) {
|
||||||
|
should_expect_colon = true;
|
||||||
|
}
|
||||||
|
if should_expect_colon {
|
||||||
|
self.expect_case_label_colon(switch_context, case_position, case_expression_span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_case_label_colon(
|
||||||
|
&mut self,
|
||||||
|
switch_context: SwitchDiagnosticContext,
|
||||||
|
case_position: TokenPosition,
|
||||||
|
case_expression_span: Option<TokenSpan>,
|
||||||
|
) {
|
||||||
|
// If the colon is missing, skip to a statement-or-stronger boundary so
|
||||||
|
// the damaged label is not parsed as arm body.
|
||||||
|
let missing_colon_error = self
|
||||||
|
.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
|
||||||
|
.widen_error_span_from(case_position)
|
||||||
|
.related_token("case_keyword", case_position);
|
||||||
|
let missing_colon_error = if let Some(case_expression_span) = case_expression_span {
|
||||||
|
missing_colon_error.related("case_expression", case_expression_span)
|
||||||
|
} else {
|
||||||
|
missing_colon_error
|
||||||
|
};
|
||||||
|
switch_context
|
||||||
|
.attach_to_result(missing_colon_error)
|
||||||
|
.sync_error_until(self, SyncLevel::StatementStart)
|
||||||
.report_error(self);
|
.report_error(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to allocate a `Switch` expression with the given span.
|
/// Parses the statements belonging to the current switch section.
|
||||||
|
///
|
||||||
|
/// Returns [`SwitchSectionBodyExit::AtSectionBoundary`] when parsing stops
|
||||||
|
/// at `case`, `default`, `}`, or end-of-file. Returns
|
||||||
|
/// [`SwitchSectionBodyExit::RecoveredAtSwitchBoundary`] when statement
|
||||||
|
/// recovery has to synchronize to the switch's closing delimiter.
|
||||||
|
fn parse_switch_section_body(
|
||||||
|
&mut self,
|
||||||
|
left_brace_position: TokenPosition,
|
||||||
|
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
|
||||||
|
) -> SwitchSectionBodyExit {
|
||||||
|
while let Some((token, token_position)) = self.peek_token_and_position() {
|
||||||
|
match token {
|
||||||
|
Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => {
|
||||||
|
return SwitchSectionBodyExit::AtSectionBoundary;
|
||||||
|
}
|
||||||
|
// Boundaries outside this switch are left to block-item parsing
|
||||||
|
// so it can attach the more specific item-level diagnostic.
|
||||||
|
_ => match self.parse_and_append_next_block_item(statements) {
|
||||||
|
Ok(()) => {
|
||||||
|
// Guard against parser bugs that would otherwise leave
|
||||||
|
// switch parsing stuck on the same token.
|
||||||
|
self.ensure_forward_progress(token_position);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
// Item recovery could not find a local boundary, so
|
||||||
|
// recover at the switch's matching closing brace.
|
||||||
|
let error =
|
||||||
|
error.sync_error_at_matching_delimiter(self, left_brace_position);
|
||||||
|
let error_statement = error.fallback(self);
|
||||||
|
statements.push(error_statement);
|
||||||
|
return SwitchSectionBodyExit::RecoveredAtSwitchBoundary;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchSectionBodyExit::AtSectionBoundary
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_delayed_switch_errors(&mut self, state: &SwitchParseState<'src, 'arena>) {
|
||||||
|
self.report_duplicate_switch_defaults(state);
|
||||||
|
self.report_switch_cases_after_default(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_duplicate_switch_defaults(&mut self, state: &SwitchParseState<'src, 'arena>) {
|
||||||
|
let Some((first_default_position, duplicate_positions)) =
|
||||||
|
state.default_keyword_positions.split_first()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(first_duplicate_position) = duplicate_positions.first().copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut error = state.diagnostic_context().attach_to_error(
|
||||||
|
self.make_error_at(
|
||||||
|
ParseErrorKind::SwitchDuplicateDefault,
|
||||||
|
first_duplicate_position,
|
||||||
|
)
|
||||||
|
.related_token("first_default", *first_default_position),
|
||||||
|
);
|
||||||
|
for (index, duplicate_position) in duplicate_positions.iter().copied().enumerate() {
|
||||||
|
error = error.related_token(
|
||||||
|
format!("duplicate_default_{}", index + 1),
|
||||||
|
duplicate_position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
error.report(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_switch_cases_after_default(&mut self, state: &SwitchParseState<'src, 'arena>) {
|
||||||
|
let Some(first_default_position) = state.first_default_keyword_position() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(first_case_position) = state.case_keyword_positions_after_default.first().copied()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut error = state.diagnostic_context().attach_to_error(
|
||||||
|
self.make_error_at(ParseErrorKind::SwitchCasesAfterDefault, first_case_position)
|
||||||
|
.related_token("first_default", first_default_position),
|
||||||
|
);
|
||||||
|
for (index, case_position) in state
|
||||||
|
.case_keyword_positions_after_default
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
error = error.related_token(format!("case_after_default_{}", index + 1), case_position);
|
||||||
|
}
|
||||||
|
error.report(self);
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn alloc_switch_node(
|
fn alloc_switch_node(
|
||||||
&self,
|
&self,
|
||||||
selector: ExpressionRef<'src, 'arena>,
|
selector: ExpressionRef<'src, 'arena>,
|
||||||
cases: ArenaVec<'arena, crate::ast::SwitchCaseRef<'src, 'arena>>,
|
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
|
||||||
default_arm: Option<ArenaVec<'arena, StatementRef<'src, 'arena>>>,
|
default_arm: Option<StatementList<'src, 'arena>>,
|
||||||
span: TokenSpan,
|
span: TokenSpan,
|
||||||
) -> ExpressionRef<'src, 'arena> {
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
self.arena.alloc_node(
|
self.arena.alloc_node(
|
||||||
crate::ast::Expression::Switch {
|
ast::Expression::Switch {
|
||||||
selector,
|
selector,
|
||||||
cases,
|
cases,
|
||||||
default_arm,
|
default_arm,
|
||||||
@ -178,23 +510,28 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
|||||||
span,
|
span,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn alloc_switch_node_from_state(
|
||||||
|
&self,
|
||||||
|
state: SwitchParseState<'src, 'arena>,
|
||||||
|
) -> ExpressionRef<'src, 'arena> {
|
||||||
|
self.alloc_switch_node(state.selector, state.cases, state.default_arm, state.span)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes an [`AstSpan`] covering a `case` group.
|
/// Computes the span of a `case` section.
|
||||||
///
|
///
|
||||||
/// The span begins at `labels_start_position` and extends to:
|
/// The span starts at the first `case` label and extends through the section
|
||||||
/// - the end of the last statement in `body`, if present; otherwise
|
/// body, or through the last label for an empty section.
|
||||||
/// - the end of the last label in `labels`, if present.
|
|
||||||
///
|
|
||||||
/// If both are empty, the span covers only `labels_start_position`.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn compute_case_span(
|
fn compute_switch_case_span(
|
||||||
labels_start_position: TokenPosition,
|
first_case_position: TokenPosition,
|
||||||
labels: &[ExpressionRef],
|
labels: &[ExpressionRef],
|
||||||
body: &[StatementRef],
|
statements: &[StatementRef],
|
||||||
) -> TokenSpan {
|
) -> TokenSpan {
|
||||||
let mut span = TokenSpan::new(labels_start_position);
|
let mut span = TokenSpan::new(first_case_position);
|
||||||
if let Some(last_statement) = body.last() {
|
if let Some(last_statement) = statements.last() {
|
||||||
span.extend_to(last_statement.span().end);
|
span.extend_to(last_statement.span().end);
|
||||||
} else if let Some(last_label) = labels.last() {
|
} else if let Some(last_label) = labels.last() {
|
||||||
span.extend_to(last_label.span().end);
|
span.extend_to(last_label.span().end);
|
||||||
|
|||||||
@ -45,6 +45,7 @@ pub enum SyncLevel {
|
|||||||
|
|
||||||
/// Closing `)` of a parenthesized/grouped construct.
|
/// Closing `)` of a parenthesized/grouped construct.
|
||||||
CloseParenthesis,
|
CloseParenthesis,
|
||||||
|
ColonDelimiter,
|
||||||
|
|
||||||
/// A statement boundary or statement starter.
|
/// A statement boundary or statement starter.
|
||||||
///
|
///
|
||||||
@ -59,7 +60,7 @@ pub enum SyncLevel {
|
|||||||
///
|
///
|
||||||
/// This is useful because `case` / `default` are stronger boundaries than
|
/// This is useful because `case` / `default` are stronger boundaries than
|
||||||
/// ordinary statements inside switch parsing.
|
/// ordinary statements inside switch parsing.
|
||||||
SwitchArmStart,
|
SwitchSectionBoundary,
|
||||||
|
|
||||||
/// Start of a declaration-like item.
|
/// Start of a declaration-like item.
|
||||||
///
|
///
|
||||||
@ -79,8 +80,9 @@ impl SyncLevel {
|
|||||||
const fn for_token(token: Token) -> Option<Self> {
|
const fn for_token(token: Token) -> Option<Self> {
|
||||||
use crate::lexer::Keyword;
|
use crate::lexer::Keyword;
|
||||||
use SyncLevel::{
|
use SyncLevel::{
|
||||||
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart,
|
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, ColonDelimiter,
|
||||||
ExpressionStart, ListSeparator, StatementStart, StatementTerminator, SwitchArmStart,
|
DeclarationStart, ExpressionStart, ListSeparator, StatementStart, StatementTerminator,
|
||||||
|
SwitchSectionBoundary,
|
||||||
};
|
};
|
||||||
|
|
||||||
match token {
|
match token {
|
||||||
@ -106,10 +108,12 @@ impl SyncLevel {
|
|||||||
| Keyword::Local,
|
| Keyword::Local,
|
||||||
) => Some(StatementStart),
|
) => Some(StatementStart),
|
||||||
|
|
||||||
|
Token::Colon => Some(ColonDelimiter),
|
||||||
|
|
||||||
Token::Semicolon => Some(StatementTerminator),
|
Token::Semicolon => Some(StatementTerminator),
|
||||||
|
|
||||||
// Switch-specific stronger boundary
|
// Switch-specific stronger boundary
|
||||||
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchArmStart),
|
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchSectionBoundary),
|
||||||
|
|
||||||
// Declaration/member starts
|
// Declaration/member starts
|
||||||
Token::Keyword(
|
Token::Keyword(
|
||||||
|
|||||||
@ -9,6 +9,7 @@ mod block_items;
|
|||||||
mod control_flow_expressions;
|
mod control_flow_expressions;
|
||||||
mod primary_expressions;
|
mod primary_expressions;
|
||||||
mod selector_expressions;
|
mod selector_expressions;
|
||||||
|
mod switch_expressions;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) struct ExpectedLabel {
|
pub(super) struct ExpectedLabel {
|
||||||
|
|||||||
@ -397,7 +397,7 @@ pub(super) const P0033_FIXTURES: &[Fixture] = &[
|
|||||||
},
|
},
|
||||||
Fixture {
|
Fixture {
|
||||||
label: "files/P0033_02.uc",
|
label: "files/P0033_02.uc",
|
||||||
source: "{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n",
|
source: "{\n Func\n (A\n #,\n B);\n Log(\"after\");\n}\n",
|
||||||
},
|
},
|
||||||
Fixture {
|
Fixture {
|
||||||
label: "files/P0033_03.uc",
|
label: "files/P0033_03.uc",
|
||||||
@ -670,7 +670,7 @@ fn check_p0033_fixtures() {
|
|||||||
assert_diagnostic(
|
assert_diagnostic(
|
||||||
&runs.get_any("files/P0033_02.uc"),
|
&runs.get_any("files/P0033_02.uc"),
|
||||||
&ExpectedDiagnostic {
|
&ExpectedDiagnostic {
|
||||||
headline: "expected `,` or `)` after argument, found `:`",
|
headline: "expected `,` or `)` after argument, found `#`",
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: Some("P0033"),
|
code: Some("P0033"),
|
||||||
primary_label: Some(ExpectedLabel {
|
primary_label: Some(ExpectedLabel {
|
||||||
@ -678,7 +678,7 @@ fn check_p0033_fixtures() {
|
|||||||
start: TokenPosition(10),
|
start: TokenPosition(10),
|
||||||
end: TokenPosition(10),
|
end: TokenPosition(10),
|
||||||
},
|
},
|
||||||
message: "unexpected `:`",
|
message: "unexpected `#`",
|
||||||
}),
|
}),
|
||||||
secondary_labels: &[
|
secondary_labels: &[
|
||||||
ExpectedLabel {
|
ExpectedLabel {
|
||||||
|
|||||||
1506
rottlib/tests/parser_diagnostics/switch_expressions.rs
Normal file
1506
rottlib/tests/parser_diagnostics/switch_expressions.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user