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
|
||||
/// recovery behavior, not full parser behavior.
|
||||
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",
|
||||
"{\n Func(A B);\n Log(\"after\");\n}\n",
|
||||
"files/P0035_02.uc",
|
||||
"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",
|
||||
"{\n Func\n (A 123);\n Log(\"after\");\n}\n",
|
||||
"files/P0035_05.uc",
|
||||
"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",
|
||||
"{\n Func(\n A\n new SomeClass\n );\n Log(\"after\");\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/P0036_02.uc",
|
||||
"switch\n(A)\n{\n case\n 1\n default:\n}\n",
|
||||
),
|
||||
(
|
||||
"files/P0033_02.uc",
|
||||
"{\n Func\n (A\n :,\n B);\n Log(\"after\");\n}\n",
|
||||
"files/P0036_03.uc",
|
||||
"switch(A) {\n case (A)\n case B:\n}\n",
|
||||
),
|
||||
(
|
||||
"files/P0033_03.uc",
|
||||
"{\n Func(\n A ?\n , B\n );\n Log(\"after\");\n}\n",
|
||||
"files/P0036_04.uc",
|
||||
"switch\n(\n A\n)\n{\n case\n A + B\n default\n :\n}\n",
|
||||
),
|
||||
(
|
||||
"files/P0033_04.uc",
|
||||
"{\n Func\n (\n A\n #,\n B\n );\n Log(\"after\");\n}\n",
|
||||
"files/P0036_05.uc",
|
||||
"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 primary_expressions;
|
||||
mod selector_expressions;
|
||||
mod switch_expressions;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FoundAt<'src> {
|
||||
@ -78,6 +79,7 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
|
||||
use control_flow_expressions::*;
|
||||
use primary_expressions::*;
|
||||
use selector_expressions::*;
|
||||
use switch_expressions::*;
|
||||
match error.kind {
|
||||
// primary_expressions.rs
|
||||
ParseErrorKind::ParenthesizedExpressionInvalidStart => {
|
||||
@ -165,6 +167,28 @@ pub(crate) fn diagnostic_from_parse_error<'src>(
|
||||
ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList => {
|
||||
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))
|
||||
.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;
|
||||
|
||||
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`
|
||||
--> rottlib/src/ast/mod.rs:80:13
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{lexer::TokenSpan, lexer::TokenPosition};
|
||||
use crate::{lexer::TokenPosition, lexer::TokenSpan};
|
||||
|
||||
/// Internal parse error kinds.
|
||||
///
|
||||
@ -82,6 +82,24 @@ pub enum ParseErrorKind {
|
||||
FunctionCallMissingClosingParenthesis,
|
||||
/// P0033
|
||||
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! ==================
|
||||
/// Found an unexpected token while parsing an expression.
|
||||
ExpressionUnexpectedToken,
|
||||
@ -99,17 +117,6 @@ pub enum ParseErrorKind {
|
||||
TypeSpecClassMissingInnerType,
|
||||
TypeSpecClassMissingClosingAngle,
|
||||
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.
|
||||
UnexpectedEndOfFile,
|
||||
/// 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
|
||||
/// 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.
|
||||
/// On success, it appends exactly one statement and advances past at least
|
||||
/// one token. If statement cannot 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 StatementList<'src, 'arena>,
|
||||
@ -126,23 +126,30 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
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(
|
||||
&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(
|
||||
let expression_result = self.parse_required_expression(
|
||||
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.
|
||||
// Without progress, a fallback statement would violate this
|
||||
// function's success contract and could leave the enclosing
|
||||
// block loop stuck.
|
||||
if expression_recovery_made_no_progress {
|
||||
return self.recover_bad_block_item_start_as_error_statement(error);
|
||||
}
|
||||
|
||||
@ -1,4 +1,28 @@
|
||||
//! 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::lexer::{self, Token, TokenPosition};
|
||||
@ -115,7 +139,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
&& next_token != terminator_token
|
||||
{
|
||||
Some(
|
||||
self.parse_expression_with_start_error(
|
||||
self.parse_required_expression_with_context(
|
||||
invalid_start_error_kind,
|
||||
for_keyword_position,
|
||||
component_start_anchor_position,
|
||||
|
||||
@ -18,9 +18,9 @@
|
||||
//! legacy cut-off rule:
|
||||
//!
|
||||
//! If the condition begins with a parenthesized expression, and the token after
|
||||
//! the matching `)` is identifier-like, the parenthesized expression is treated
|
||||
//! as the whole condition. The following identifier-like token is left for the
|
||||
//! branch body.
|
||||
//! the matching `)` is identifier-like, the parser treats only the
|
||||
//! parenthesized expression as the condition. The following identifier-like
|
||||
//! token is left for the branch body.
|
||||
//!
|
||||
//! This prevents the parser from accidentally consuming the following
|
||||
//! 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
|
||||
//! `Cross` as a continuation of the condition in dialects where identifier-like
|
||||
//! tokens may participate in operator syntax.
|
||||
//! the identifier-like body opener, such as `Cross`, as an operator-like
|
||||
//! continuation of the parenthesized condition.
|
||||
//!
|
||||
//! Operator tokens such as `*`, `+`, `<`, `==`, etc. do not trigger this
|
||||
//! 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
|
||||
//! ends the condition at the closing `)`. Write the condition with additional
|
||||
//! 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::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
||||
@ -210,7 +173,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
branch_owner_keyword_position: TokenPosition,
|
||||
) -> BranchBody<'src, 'arena> {
|
||||
let branch_expression = self
|
||||
.parse_expression_with_start_error(
|
||||
.parse_required_expression_with_context(
|
||||
ParseErrorKind::ControlFlowBodyExpected,
|
||||
branch_owner_keyword_position,
|
||||
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)),
|
||||
_ => {
|
||||
let return_value = self
|
||||
.parse_expression_with_start_error(
|
||||
.parse_required_expression(
|
||||
ParseErrorKind::ReturnValueInvalidStart,
|
||||
return_keyword_position,
|
||||
return_keyword_position,
|
||||
)
|
||||
.unwrap_or_fallback(self);
|
||||
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)),
|
||||
_ => {
|
||||
let break_value = self
|
||||
.parse_expression_with_start_error(
|
||||
.parse_required_expression(
|
||||
ParseErrorKind::BreakValueInvalidStart,
|
||||
break_keyword_position,
|
||||
break_keyword_position,
|
||||
)
|
||||
.unwrap_or_fallback(self);
|
||||
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
|
||||
/// which the expression was expected. It is attached to the diagnostic with
|
||||
/// the [`diagnostic_labels::EXPRESSION_EXPECTED_AFTER`] label.
|
||||
pub(super) fn parse_expression_with_start_error(
|
||||
pub(super) fn parse_required_expression(
|
||||
&mut self,
|
||||
bad_start_error_kind: ParseErrorKind,
|
||||
required_by_position: crate::lexer::TokenPosition,
|
||||
expression_context_position: crate::lexer::TokenPosition,
|
||||
required_by_position: 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> {
|
||||
if self.next_token_definitely_cannot_start_expression() {
|
||||
let error_position = self.peek_position_or_eof();
|
||||
//self.advance();
|
||||
|
||||
return Err(self
|
||||
.make_error_at(bad_start_error_kind, error_position)
|
||||
@ -109,6 +129,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
expression_context_position,
|
||||
));
|
||||
}
|
||||
|
||||
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;
|
||||
// this prevents error cases like `new(Outer, Name, 7 +) SomeClass`.
|
||||
if token.is_definitely_not_expression_start() {
|
||||
return Err(
|
||||
self.make_error_at(ParseErrorKind::ExpressionExpected, token_position)
|
||||
);
|
||||
return Err(self.make_error_at(ParseErrorKind::ExpressionExpected, token_position));
|
||||
}
|
||||
self.advance();
|
||||
if let Ok(operator) = ast::PrefixOperator::try_from(token) {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! This module implements parsing of primary expressions via
|
||||
//! [`Parser::parse_primary_from_current_token`] and its helper
|
||||
//! [`Parser::parse_keyword_primary`].
|
||||
//! [`Parser::try_parse_keyword_primary`].
|
||||
//!
|
||||
//! ## What is a "primary expression" here?
|
||||
//!
|
||||
@ -25,6 +25,34 @@
|
||||
//! So "primary" here does not mean "smallest atomic expression".
|
||||
//! It means "an expression form that does not need a left-hand side
|
||||
//! 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::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
|
||||
/// `new(...)` argument list has been parsed or recovered.
|
||||
#[must_use]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
enum NewClassSpecifierParseAction {
|
||||
Parse,
|
||||
|
||||
@ -104,7 +104,7 @@ impl<'src, 'arena> Parser<'src, 'arena> {
|
||||
left_bracket_position: TokenPosition,
|
||||
) -> ParseExpressionResult<'src, 'arena> {
|
||||
let index_expression = self
|
||||
.parse_expression_with_start_error(
|
||||
.parse_required_expression_with_context(
|
||||
ParseErrorKind::IndexMissingExpression,
|
||||
left_hand_side.span().end,
|
||||
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.
|
||||
use crate::arena::ArenaVec;
|
||||
use crate::ast::{ExpressionRef, StatementRef};
|
||||
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
|
||||
use crate::parser::{ParseErrorKind, ResultRecoveryExt};
|
||||
//! Dispatch into this module happens only after `primary.rs` has committed to
|
||||
//! keyword-led `switch` syntax. That commitment is purely syntactic: `switch`
|
||||
//! followed by `(` is treated as a switch expression; otherwise `switch` may
|
||||
//! still be parsed as an identifier-like primary.
|
||||
//!
|
||||
//! 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> {
|
||||
/// Parses a `switch` expression after the `switch` keyword has been
|
||||
/// consumed.
|
||||
use crate::arena::ArenaVec;
|
||||
use crate::ast::{self, ExpressionRef, StatementList, StatementRef, SwitchCaseRef};
|
||||
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
|
||||
/// entire construct, from `switch_start_position` to the closing `}`.
|
||||
/// If the switch is closed normally, returns an [`ast::Expression::Switch`]
|
||||
/// whose span covers the construct from `switch_start_position` through the
|
||||
/// closing `}`.
|
||||
///
|
||||
/// Only one `default` arm is recorded. Duplicate defaults and `case` arms
|
||||
/// after a `default` are reported as errors.
|
||||
///
|
||||
/// On premature end-of-file, reports an error and returns a best-effort
|
||||
/// switch node.
|
||||
/// On errors, reports diagnostics and returns a best-effort switch node.
|
||||
#[must_use]
|
||||
pub(crate) fn parse_switch_tail(
|
||||
&mut self,
|
||||
switch_start_position: TokenPosition,
|
||||
) -> ExpressionRef<'src, 'arena> {
|
||||
let selector = self.parse_expression();
|
||||
let mut cases = self.arena.vec();
|
||||
let mut default_arm = None;
|
||||
let mut span = TokenSpan::new(switch_start_position);
|
||||
if self
|
||||
.expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
|
||||
.report_error(self)
|
||||
{
|
||||
return self.alloc_switch_node(selector, cases, default_arm, span);
|
||||
let mut state = match self.parse_switch_header_tail(switch_start_position) {
|
||||
Ok(state) => state,
|
||||
Err(switch_node) => return switch_node,
|
||||
};
|
||||
match self.parse_switch_sections_tail(&mut state) {
|
||||
SwitchSectionsExit::ClosedByRightBrace | SwitchSectionsExit::ClosedByRecovery => {
|
||||
self.report_delayed_switch_errors(&state);
|
||||
self.alloc_switch_node_from_state(state)
|
||||
}
|
||||
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() {
|
||||
match token {
|
||||
let body_exit = match token {
|
||||
Token::RightBrace => {
|
||||
self.advance(); // '}'
|
||||
span.extend_to(token_position);
|
||||
return self.alloc_switch_node(selector, cases, default_arm, span);
|
||||
state.span.extend_to(token_position);
|
||||
return SwitchSectionsExit::ClosedByRightBrace;
|
||||
}
|
||||
Token::Keyword(Keyword::Case) => {
|
||||
if default_arm.is_some() {
|
||||
self.report_error_here(ParseErrorKind::SwitchCasesAfterDefault);
|
||||
}
|
||||
let case_node = self.parse_switch_case_group(token_position);
|
||||
cases.push(case_node);
|
||||
self.parse_case_section_into_state(state, token_position)
|
||||
}
|
||||
Token::Keyword(Keyword::Default) => {
|
||||
if default_arm.is_some() {
|
||||
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()),
|
||||
);
|
||||
self.parse_default_section_into_state(state, token_position)
|
||||
}
|
||||
// Items before the first arm declaration are not allowed, but
|
||||
// are parsed for basic diagnostics and simplicity.
|
||||
_ => self.parse_switch_preamble_items(token_position),
|
||||
// Invalid switch-level items are parsed before being discarded
|
||||
// so ordinary statement diagnostics still run.
|
||||
_ => 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.report_error_here(ParseErrorKind::SwitchMissingClosingBrace);
|
||||
// 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)
|
||||
SwitchSectionsExit::EndOfFile
|
||||
}
|
||||
|
||||
/// Parses a stacked `case` group and its body:
|
||||
/// `case <expr>: (case <expr>:)* <arm-body>`.
|
||||
/// Parses a `case` section and appends it to `state`.
|
||||
///
|
||||
/// 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
|
||||
/// `first_case_position` to the end of the arm body, or to the end of the
|
||||
/// last label if the body is empty.
|
||||
#[must_use]
|
||||
fn parse_switch_case_group(
|
||||
/// Returns the boundary that stopped body parsing.
|
||||
fn parse_case_section_into_state(
|
||||
&mut self,
|
||||
state: &mut SwitchParseState<'src, 'arena>,
|
||||
first_case_position: TokenPosition,
|
||||
) -> crate::ast::SwitchCaseRef<'src, 'arena> {
|
||||
let mut labels = self.arena.vec();
|
||||
while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
|
||||
self.advance(); // 'case'
|
||||
labels.push(self.parse_expression());
|
||||
|
||||
// `:` 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);
|
||||
) -> SwitchSectionBodyExit {
|
||||
if state.has_default() {
|
||||
state
|
||||
.case_keyword_positions_after_default
|
||||
.push(first_case_position);
|
||||
}
|
||||
let mut body = self.arena.vec();
|
||||
self.parse_switch_arm_body(&mut body);
|
||||
let case_span = compute_case_span(first_case_position, &labels, &body);
|
||||
self.arena
|
||||
.alloc_node(crate::ast::SwitchCase { labels, body }, case_span)
|
||||
let switch_context = state.diagnostic_context();
|
||||
let labels = self.parse_case_labels(switch_context);
|
||||
let mut statements = self.arena.vec();
|
||||
let body_exit =
|
||||
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`.
|
||||
fn parse_switch_default_arm(
|
||||
/// Parses a `default:` section into `state`.
|
||||
///
|
||||
/// 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,
|
||||
state: &mut SwitchParseState<'src, 'arena>,
|
||||
default_position: TokenPosition,
|
||||
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
|
||||
) {
|
||||
) -> SwitchSectionBodyExit {
|
||||
self.advance(); // 'default'
|
||||
self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
|
||||
.widen_error_span_from(default_position)
|
||||
.sync_error_until(self, crate::parser::SyncLevel::StatementStart)
|
||||
state.default_keyword_positions.push(default_position);
|
||||
let switch_context = state.diagnostic_context();
|
||||
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);
|
||||
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.
|
||||
fn parse_switch_arm_body(
|
||||
/// Parses invalid switch-level items up to the next section boundary.
|
||||
///
|
||||
/// Parsed statements are discarded after diagnostics; they are not part of
|
||||
/// any switch arm.
|
||||
fn parse_invalid_switch_preamble(
|
||||
&mut self,
|
||||
statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
|
||||
) {
|
||||
while let Some((token, token_position)) = self.peek_token_and_position() {
|
||||
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)
|
||||
switch_context: SwitchDiagnosticContext,
|
||||
preamble_start_position: TokenPosition,
|
||||
) -> SwitchSectionBodyExit
|
||||
where
|
||||
'src: 'arena,
|
||||
{
|
||||
// Discard parsed statements into a sink vector.
|
||||
// This is a bit "hacky", but I don't want to adapt code to skip
|
||||
// production of AST nodes just to report errors in
|
||||
// one problematic case.
|
||||
let mut sink = self.arena.vec();
|
||||
self.parse_switch_arm_body(&mut sink);
|
||||
self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase)
|
||||
.widen_error_span_from(preamble_start_position)
|
||||
// Build the statements only to reuse normal statement diagnostics and
|
||||
// recovery; switch-level items cannot be represented in the switch AST.
|
||||
let mut discarded_statements = self.arena.vec();
|
||||
let body_exit = self.parse_switch_section_body(
|
||||
switch_context.left_brace_position,
|
||||
&mut discarded_statements,
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn alloc_switch_node(
|
||||
&self,
|
||||
selector: ExpressionRef<'src, 'arena>,
|
||||
cases: ArenaVec<'arena, crate::ast::SwitchCaseRef<'src, 'arena>>,
|
||||
default_arm: Option<ArenaVec<'arena, StatementRef<'src, 'arena>>>,
|
||||
cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
|
||||
default_arm: Option<StatementList<'src, 'arena>>,
|
||||
span: TokenSpan,
|
||||
) -> ExpressionRef<'src, 'arena> {
|
||||
self.arena.alloc_node(
|
||||
crate::ast::Expression::Switch {
|
||||
ast::Expression::Switch {
|
||||
selector,
|
||||
cases,
|
||||
default_arm,
|
||||
@ -178,23 +510,28 @@ impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
|
||||
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 end of the last statement in `body`, if present; otherwise
|
||||
/// - the end of the last label in `labels`, if present.
|
||||
///
|
||||
/// If both are empty, the span covers only `labels_start_position`.
|
||||
/// The span starts at the first `case` label and extends through the section
|
||||
/// body, or through the last label for an empty section.
|
||||
#[must_use]
|
||||
fn compute_case_span(
|
||||
labels_start_position: TokenPosition,
|
||||
fn compute_switch_case_span(
|
||||
first_case_position: TokenPosition,
|
||||
labels: &[ExpressionRef],
|
||||
body: &[StatementRef],
|
||||
statements: &[StatementRef],
|
||||
) -> TokenSpan {
|
||||
let mut span = TokenSpan::new(labels_start_position);
|
||||
if let Some(last_statement) = body.last() {
|
||||
let mut span = TokenSpan::new(first_case_position);
|
||||
if let Some(last_statement) = statements.last() {
|
||||
span.extend_to(last_statement.span().end);
|
||||
} else if let Some(last_label) = labels.last() {
|
||||
span.extend_to(last_label.span().end);
|
||||
|
||||
@ -45,6 +45,7 @@ pub enum SyncLevel {
|
||||
|
||||
/// Closing `)` of a parenthesized/grouped construct.
|
||||
CloseParenthesis,
|
||||
ColonDelimiter,
|
||||
|
||||
/// A statement boundary or statement starter.
|
||||
///
|
||||
@ -59,7 +60,7 @@ pub enum SyncLevel {
|
||||
///
|
||||
/// This is useful because `case` / `default` are stronger boundaries than
|
||||
/// ordinary statements inside switch parsing.
|
||||
SwitchArmStart,
|
||||
SwitchSectionBoundary,
|
||||
|
||||
/// Start of a declaration-like item.
|
||||
///
|
||||
@ -79,8 +80,9 @@ impl SyncLevel {
|
||||
const fn for_token(token: Token) -> Option<Self> {
|
||||
use crate::lexer::Keyword;
|
||||
use SyncLevel::{
|
||||
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, DeclarationStart,
|
||||
ExpressionStart, ListSeparator, StatementStart, StatementTerminator, SwitchArmStart,
|
||||
BlockBoundary, CloseAngleBracket, CloseBracket, CloseParenthesis, ColonDelimiter,
|
||||
DeclarationStart, ExpressionStart, ListSeparator, StatementStart, StatementTerminator,
|
||||
SwitchSectionBoundary,
|
||||
};
|
||||
|
||||
match token {
|
||||
@ -106,10 +108,12 @@ impl SyncLevel {
|
||||
| Keyword::Local,
|
||||
) => Some(StatementStart),
|
||||
|
||||
Token::Colon => Some(ColonDelimiter),
|
||||
|
||||
Token::Semicolon => Some(StatementTerminator),
|
||||
|
||||
// Switch-specific stronger boundary
|
||||
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchArmStart),
|
||||
Token::Keyword(Keyword::Case | Keyword::Default) => Some(SwitchSectionBoundary),
|
||||
|
||||
// Declaration/member starts
|
||||
Token::Keyword(
|
||||
|
||||
@ -9,6 +9,7 @@ mod block_items;
|
||||
mod control_flow_expressions;
|
||||
mod primary_expressions;
|
||||
mod selector_expressions;
|
||||
mod switch_expressions;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ExpectedLabel {
|
||||
|
||||
@ -397,7 +397,7 @@ pub(super) const P0033_FIXTURES: &[Fixture] = &[
|
||||
},
|
||||
Fixture {
|
||||
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 {
|
||||
label: "files/P0033_03.uc",
|
||||
@ -670,7 +670,7 @@ fn check_p0033_fixtures() {
|
||||
assert_diagnostic(
|
||||
&runs.get_any("files/P0033_02.uc"),
|
||||
&ExpectedDiagnostic {
|
||||
headline: "expected `,` or `)` after argument, found `:`",
|
||||
headline: "expected `,` or `)` after argument, found `#`",
|
||||
severity: Severity::Error,
|
||||
code: Some("P0033"),
|
||||
primary_label: Some(ExpectedLabel {
|
||||
@ -678,7 +678,7 @@ fn check_p0033_fixtures() {
|
||||
start: TokenPosition(10),
|
||||
end: TokenPosition(10),
|
||||
},
|
||||
message: "unexpected `:`",
|
||||
message: "unexpected `#`",
|
||||
}),
|
||||
secondary_labels: &[
|
||||
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