diff --git a/dev_tests/src/verify_expr.rs b/dev_tests/src/verify_expr.rs index 56168e3..3efab68 100644 --- a/dev_tests/src/verify_expr.rs +++ b/dev_tests/src/verify_expr.rs @@ -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", ), ]; diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs index 32d7537..60179e9 100644 --- a/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs +++ b/rottlib/src/diagnostics/parse_error_diagnostics/mod.rs @@ -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") diff --git a/rottlib/src/diagnostics/parse_error_diagnostics/switch_expressions.rs b/rottlib/src/diagnostics/parse_error_diagnostics/switch_expressions.rs new file mode 100644 index 0000000..fc45144 --- /dev/null +++ b/rottlib/src/diagnostics/parse_error_diagnostics/switch_expressions.rs @@ -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 { + error.related_spans.get(label).copied() +} + +fn context_span_if_useful( + file: &TokenizedFile<'_>, + context_span: Option, + blame_span: TokenSpan, +) -> Option { + context_span.filter(|span| should_show_context_label(file, *span, blame_span)) +} + +fn primary_span_with_context( + context_span: Option, + 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, +) -> Option { + 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 { + 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() +} \ No newline at end of file diff --git a/rottlib/src/diagnostics/render.rs b/rottlib/src/diagnostics/render.rs index 795fab4..8af453b 100644 --- a/rottlib/src/diagnostics/render.rs +++ b/rottlib/src/diagnostics/render.rs @@ -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 diff --git a/rottlib/src/parser/errors.rs b/rottlib/src/parser/errors.rs index 9a355fd..343187f 100644 --- a/rottlib/src/parser/errors.rs +++ b/rottlib/src/parser/errors.rs @@ -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. diff --git a/rottlib/src/parser/grammar/expression/block.rs b/rottlib/src/parser/grammar/expression/block.rs index ef54273..70ae3cc 100644 --- a/rottlib/src/parser/grammar/expression/block.rs +++ b/rottlib/src/parser/grammar/expression/block.rs @@ -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); } diff --git a/rottlib/src/parser/grammar/expression/control_flow/for_loop.rs b/rottlib/src/parser/grammar/expression/control_flow/for_loop.rs index 120c4ec..7fdb937 100644 --- a/rottlib/src/parser/grammar/expression/control_flow/for_loop.rs +++ b/rottlib/src/parser/grammar/expression/control_flow/for_loop.rs @@ -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, diff --git a/rottlib/src/parser/grammar/expression/control_flow/mod.rs b/rottlib/src/parser/grammar/expression/control_flow/mod.rs index d31dcd9..cd4a4fe 100644 --- a/rottlib/src/parser/grammar/expression/control_flow/mod.rs +++ b/rottlib/src/parser/grammar/expression/control_flow/mod.rs @@ -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); diff --git a/rottlib/src/parser/grammar/expression/pratt.rs b/rottlib/src/parser/grammar/expression/pratt.rs index 570eb5d..8c090e4 100644 --- a/rottlib/src/parser/grammar/expression/pratt.rs +++ b/rottlib/src/parser/grammar/expression/pratt.rs @@ -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) { diff --git a/rottlib/src/parser/grammar/expression/primary/mod.rs b/rottlib/src/parser/grammar/expression/primary/mod.rs index 71afce9..b5d8824 100644 --- a/rottlib/src/parser/grammar/expression/primary/mod.rs +++ b/rottlib/src/parser/grammar/expression/primary/mod.rs @@ -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}; diff --git a/rottlib/src/parser/grammar/expression/primary/new.rs b/rottlib/src/parser/grammar/expression/primary/new.rs index 503e95f..df54e54 100644 --- a/rottlib/src/parser/grammar/expression/primary/new.rs +++ b/rottlib/src/parser/grammar/expression/primary/new.rs @@ -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, diff --git a/rottlib/src/parser/grammar/expression/selectors.rs b/rottlib/src/parser/grammar/expression/selectors.rs index 0e8febd..884abc1 100644 --- a/rottlib/src/parser/grammar/expression/selectors.rs +++ b/rottlib/src/parser/grammar/expression/selectors.rs @@ -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, diff --git a/rottlib/src/parser/grammar/expression/switch.rs b/rottlib/src/parser/grammar/expression/switch.rs index 0ca0e54..eba8741 100644 --- a/rottlib/src/parser/grammar/expression/switch.rs +++ b/rottlib/src/parser/grammar/expression/switch.rs @@ -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 :` 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>, + + // 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, + case_keyword_positions_after_default: Vec, + + 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 { + 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, 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 : (case :)* `. + /// Parses a `case` section and appends it to `state`. /// - /// Returns the allocated [`crate::ast::CaseRef`] node. + /// A section may have stacked `case :` 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, + ) { + // 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>>, + cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>, + default_arm: Option>, 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); diff --git a/rottlib/src/parser/recovery.rs b/rottlib/src/parser/recovery.rs index ee5b87f..3ecc1dd 100644 --- a/rottlib/src/parser/recovery.rs +++ b/rottlib/src/parser/recovery.rs @@ -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 { 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( diff --git a/rottlib/tests/parser_diagnostics/mod.rs b/rottlib/tests/parser_diagnostics/mod.rs index 55d1686..dd7b9e3 100644 --- a/rottlib/tests/parser_diagnostics/mod.rs +++ b/rottlib/tests/parser_diagnostics/mod.rs @@ -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 { diff --git a/rottlib/tests/parser_diagnostics/selector_expressions.rs b/rottlib/tests/parser_diagnostics/selector_expressions.rs index 767e24a..8fae49c 100644 --- a/rottlib/tests/parser_diagnostics/selector_expressions.rs +++ b/rottlib/tests/parser_diagnostics/selector_expressions.rs @@ -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 { diff --git a/rottlib/tests/parser_diagnostics/switch_expressions.rs b/rottlib/tests/parser_diagnostics/switch_expressions.rs new file mode 100644 index 0000000..be9ee2f --- /dev/null +++ b/rottlib/tests/parser_diagnostics/switch_expressions.rs @@ -0,0 +1,1506 @@ +use super::*; + +pub(super) const P0034_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0034_01.uc", + source: "switch(A) local\n", + }, + Fixture { + label: "files/P0034_02.uc", + source: "switch\n(A)\nvar", + }, + Fixture { + label: "files/P0034_03.uc", + source: "switch(\n A\n)\n", + }, + Fixture { + label: "files/P0034_04.uc", + source: "switch\n(\n A\n)\n", + }, + Fixture { + label: "files/P0034_05.uc", + source: "switch(A)\ncase 1:\n", + }, +]; + +#[test] +fn check_p0034_fixtures() { + let runs = run_fixtures(P0034_FIXTURES); + + assert_eq!(runs.get("files/P0034_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0034_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0034_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0034_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0034_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0034_01.uc"), + &ExpectedDiagnostic { + headline: "missing `{` to start `switch` body", + severity: Severity::Error, + code: Some("P0034"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "expected `{` before `local`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0034_02.uc"), + &ExpectedDiagnostic { + headline: "missing `{` to start `switch` body", + severity: Severity::Error, + code: Some("P0034"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(6), + }, + message: "expected `{` before `var`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0034_03.uc"), + &ExpectedDiagnostic { + headline: "missing `{` to start `switch` body", + severity: Severity::Error, + code: Some("P0034"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(8), + }, + message: "expected `{` after the switch expression", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0034_04.uc"), + &ExpectedDiagnostic { + headline: "missing `{` to start `switch` body", + severity: Severity::Error, + code: Some("P0034"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(9), + }, + message: "expected `{` after the switch expression", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0034_05.uc"), + &ExpectedDiagnostic { + headline: "missing `{` to start `switch` body", + severity: Severity::Error, + code: Some("P0034"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(5), + }, + message: "expected `{` before `case`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0035_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0035_01.uc", + source: "switch(A) {\n Log(\"bad\");\n}\n", + }, + Fixture { + label: "files/P0035_02.uc", + source: "switch\n(A)\n{\n Log(\"bad\");\n Log(\"worse\");\n case 1:\n}\n", + }, + Fixture { + label: "files/P0035_03.uc", + source: "switch(A) {\n 123;\n default:\n}\n", + }, + Fixture { + label: "files/P0035_04.uc", + source: "switch\n(\n A\n)\n{\n if (A) {}\n case 1:\n}\n", + }, + Fixture { + label: "files/P0035_05.uc", + source: "switch(A) {\n {\n Log(\"nested\");\n }\n case 1:\n}\n", + }, +]; + +#[test] +fn check_p0035_fixtures() { + let runs = run_fixtures(P0035_FIXTURES); + + assert_eq!(runs.get("files/P0035_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0035_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0035_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0035_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0035_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0035_01.uc"), + &ExpectedDiagnostic { + headline: "expected `case` or `default` section label in switch body", + severity: Severity::Error, + code: Some("P0035"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(12), + }, + message: "this statement must be inside a `case` or `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "switch body starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0035_02.uc"), + &ExpectedDiagnostic { + headline: "expected `case` or `default` section label in switch body", + severity: Severity::Error, + code: Some("P0035"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(20), + }, + message: "these statements must be inside a `case` or `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`switch` starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(6), + }, + message: "switch body starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0035_03.uc"), + &ExpectedDiagnostic { + headline: "expected `case` or `default` section label in switch body", + severity: Severity::Error, + code: Some("P0035"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(9), + }, + message: "this statement must be inside a `case` or `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "switch body starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0035_04.uc"), + &ExpectedDiagnostic { + headline: "expected `case` or `default` section label in switch body", + severity: Severity::Error, + code: Some("P0035"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(19), + }, + message: "this statement must be inside a `case` or `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`switch` starts here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "switch body starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0035_05.uc"), + &ExpectedDiagnostic { + headline: "expected `case` or `default` section label in switch body", + severity: Severity::Error, + code: Some("P0035"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(18), + }, + message: "this block must be inside a `case` or `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(5), + }, + message: "switch body starts here", + }, + ], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0036_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0036_01.uc", + source: "switch(A) {\n case 1\n case 2:\n}\n", + }, + Fixture { + label: "files/P0036_02.uc", + source: "switch\n(A)\n{\n case\n 1\n default:\n}\n", + }, + Fixture { + label: "files/P0036_03.uc", + source: "switch(A) {\n case (A)\n case B:\n}\n", + }, + Fixture { + label: "files/P0036_04.uc", + source: "switch\n(\n A\n)\n{\n case\n A + B\n default\n :\n}\n", + }, + Fixture { + label: "files/P0036_05.uc", + source: "switch(A) {\n case Foo.Bar(Baz)\n case Other:\n}\n", + }, +]; + +#[test] +fn check_p0036_fixtures() { + let runs = run_fixtures(P0036_FIXTURES); + + assert_eq!(runs.get("files/P0036_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0036_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0036_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0036_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0036_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0036_01.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `case` label", + severity: Severity::Error, + code: Some("P0036"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(13), + end: TokenPosition(13), + }, + message: "expected `:` before `case`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(10), + }, + message: "this `case` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0036_02.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `case` label", + severity: Severity::Error, + code: Some("P0036"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "expected `:` before `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(12), + }, + message: "this `case` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0036_03.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `case` label", + severity: Severity::Error, + code: Some("P0036"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "expected `:` before `case`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(12), + }, + message: "this `case` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0036_04.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `case` label", + severity: Severity::Error, + code: Some("P0036"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(22), + end: TokenPosition(22), + }, + message: "expected `:` before `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(19), + }, + message: "this `case` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0036_05.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `case` label", + severity: Severity::Error, + code: Some("P0036"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(18), + end: TokenPosition(18), + }, + message: "expected `:` before `case`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(15), + }, + message: "this `case` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0037_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0037_01.uc", + source: "switch(A) {\n default\n if (A) {}\n}\n", + }, + Fixture { + label: "files/P0037_02.uc", + source: "switch\n(A)\n{\n default\n while (A) {}\n}\n", + }, + Fixture { + label: "files/P0037_03.uc", + source: "switch(A) {\n default\n for (;;) {}\n}\n", + }, + Fixture { + label: "files/P0037_04.uc", + source: "switch\n(\n A\n)\n{\n default\n switch(B) {\n case 1:\n }\n}\n", + }, + Fixture { + label: "files/P0037_05.uc", + source: "switch(A) {\n default\n case 1:\n}\n", + }, +]; + +#[test] +fn check_p0037_fixtures() { + let runs = run_fixtures(P0037_FIXTURES); + + assert_eq!(runs.get("files/P0037_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0037_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0037_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0037_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0037_05.uc").unwrap().len(), 2); + + assert_diagnostic( + &runs.get_any("files/P0037_01.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `default`", + severity: Severity::Error, + code: Some("P0037"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "expected `:` before `if`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "this `default` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0037_02.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `default`", + severity: Severity::Error, + code: Some("P0037"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "expected `:` before `while`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "this `default` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0037_03.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `default`", + severity: Severity::Error, + code: Some("P0037"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "expected `:` before `for`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "this `default` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0037_04.uc"), + &ExpectedDiagnostic { + headline: "missing `:` after `default`", + severity: Severity::Error, + code: Some("P0037"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "expected `:` before `switch`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "this `default` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0037_05.uc", "P0037"), + &ExpectedDiagnostic { + headline: "missing `:` after `default`", + severity: Severity::Error, + code: Some("P0037"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "expected `:` before `case`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "this `default` label needs a trailing `:`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_by_code("files/P0037_05.uc", "P0039"), + &ExpectedDiagnostic { + headline: "`case` section appears after `default`", + severity: Severity::Error, + code: Some("P0039"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "`case` after `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "`default` must be the last section in this switch", + }, + ], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0038_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0038_01.uc", + source: "switch(A) {\n default:\n default:\n}\n", + }, + Fixture { + label: "files/P0038_02.uc", + source: "switch\n(A)\n{\n default\n :\n default\n :\n}\n", + }, + Fixture { + label: "files/P0038_03.uc", + source: "switch(A) {\n default:\n default:\n default:\n}\n", + }, + Fixture { + label: "files/P0038_04.uc", + source: "switch\n(\n A\n)\n{\n default:\n Log(\"first\");\n default:\n Log(\"second\");\n}\n", + }, +]; + +#[test] +fn check_p0038_fixtures() { + let runs = run_fixtures(P0038_FIXTURES); + + assert_eq!(runs.get("files/P0038_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0038_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0038_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0038_04.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0038_01.uc"), + &ExpectedDiagnostic { + headline: "duplicate `default` section in switch", + severity: Severity::Error, + code: Some("P0038"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "duplicate `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "first `default` section is here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0038_02.uc"), + &ExpectedDiagnostic { + headline: "duplicate `default` section in switch", + severity: Severity::Error, + code: Some("P0038"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "duplicate `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "first `default` section is here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0038_03.uc"), + &ExpectedDiagnostic { + headline: "multiple `default` sections in switch", + severity: Severity::Error, + code: Some("P0038"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "duplicate `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "first `default` section is here", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(16), + end: TokenPosition(16), + }, + message: "another duplicate `default` section", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0038_04.uc"), + &ExpectedDiagnostic { + headline: "duplicate `default` section in switch", + severity: Severity::Error, + code: Some("P0038"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(23), + end: TokenPosition(23), + }, + message: "duplicate `default` section", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "first `default` section is here", + }, + ], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0039_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0039_01.uc", + source: "switch(A) {\n default:\n case 1:\n}\n", + }, + Fixture { + label: "files/P0039_02.uc", + source: "switch\n(A)\n{\n default\n :\n case\n 1\n :\n}\n", + }, + Fixture { + label: "files/P0039_03.uc", + source: "switch(A) {\n default:\n case 1:\n case 2:\n}\n", + }, + Fixture { + label: "files/P0039_04.uc", + source: "switch\n(\n A\n)\n{\n default:\n case 1:\n Log(\"one\");\n case 2:\n Log(\"two\");\n}\n", + }, + Fixture { + label: "files/P0039_05.uc", + source: "switch(A) {\n default:\n Log(\"done\");\n case 1:\n case 2:\n Log(\"stacked\");\n}\n", + }, +]; + +#[test] +fn check_p0039_fixtures() { + let runs = run_fixtures(P0039_FIXTURES); + + assert_eq!(runs.get("files/P0039_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0039_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0039_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0039_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0039_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0039_01.uc"), + &ExpectedDiagnostic { + headline: "`case` section appears after `default`", + severity: Severity::Error, + code: Some("P0039"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "`case` after `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "`default` must be the last section in this switch", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0039_02.uc"), + &ExpectedDiagnostic { + headline: "`case` section appears after `default`", + severity: Severity::Error, + code: Some("P0039"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "`case` after `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "`default` must be the last section in this switch", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0039_03.uc"), + &ExpectedDiagnostic { + headline: "`case` section appears after `default`", + severity: Severity::Error, + code: Some("P0039"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "`case` after `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "`default` must be the last section in this switch", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0039_04.uc"), + &ExpectedDiagnostic { + headline: "multiple `case` sections appear after `default`", + severity: Severity::Error, + code: Some("P0039"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(16), + end: TokenPosition(16), + }, + message: "`case` after `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "`default` must be the last section in this switch", + }, + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(29), + end: TokenPosition(29), + }, + message: "another `case` after `default`", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0039_05.uc"), + &ExpectedDiagnostic { + headline: "`case` section appears after `default`", + severity: Severity::Error, + code: Some("P0039"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(19), + end: TokenPosition(19), + }, + message: "`case` after `default`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "`default` must be the last section in this switch", + }, + ], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0040_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0040_01.uc", + source: "switch(A) {\n", + }, + Fixture { + label: "files/P0040_02.uc", + source: "switch(A) {\n case 1:\n", + }, + Fixture { + label: "files/P0040_03.uc", + source: "switch\n(A)\n{\n default:\n", + }, + Fixture { + label: "files/P0040_04.uc", + source: "switch\n(\n A\n)\n{\n case 1:\n case 2:\n", + }, + Fixture { + label: "files/P0040_05.uc", + source: "switch(A) {\n case 1:\n Log(\"body\");\n", + }, +]; + +#[test] +fn check_p0040_fixtures() { + let runs = run_fixtures(P0040_FIXTURES); + + assert_eq!(runs.get("files/P0040_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0040_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0040_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0040_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0040_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0040_01.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close switch body", + severity: Severity::Error, + code: Some("P0040"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(7), + end: TokenPosition(7), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0040_02.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close switch body", + severity: Severity::Error, + code: Some("P0040"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(13), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0040_03.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close switch body", + severity: Severity::Error, + code: Some("P0040"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(6), + end: TokenPosition(12), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`switch` starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0040_04.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close switch body", + severity: Severity::Error, + code: Some("P0040"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(23), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(0), + end: TokenPosition(0), + }, + message: "`switch` starts here", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0040_05.uc"), + &ExpectedDiagnostic { + headline: "missing `}` to close switch body", + severity: Severity::Error, + code: Some("P0040"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(5), + end: TokenPosition(20), + }, + message: "expected `}` before end of file", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0041_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0041_01.uc", + source: "switch(A) {\n case:\n}\n", + }, + Fixture { + label: "files/P0041_02.uc", + source: "switch\n(A)\n{\n case\n :\n}\n", + }, + Fixture { + label: "files/P0041_03.uc", + source: "switch(A) {\n case:\n default:\n}\n", + }, + Fixture { + label: "files/P0041_04.uc", + source: "switch\n(\n A\n)\n{\n case\n :\n case 1:\n}\n", + }, + Fixture { + label: "files/P0041_05.uc", + source: "switch(A) {\n case\n :\n default:\n}\n", + }, +]; + +#[test] +fn check_p0041_fixtures() { + let runs = run_fixtures(P0041_FIXTURES); + + assert_eq!(runs.get("files/P0041_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0041_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0041_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0041_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0041_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0041_01.uc"), + &ExpectedDiagnostic { + headline: "missing expression after `case`", + severity: Severity::Error, + code: Some("P0041"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "expected expression before `:`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0041_02.uc"), + &ExpectedDiagnostic { + headline: "missing expression after `case`", + severity: Severity::Error, + code: Some("P0041"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "expected expression before `:`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "after this `case`, an expression was expected", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0041_03.uc"), + &ExpectedDiagnostic { + headline: "missing expression after `case`", + severity: Severity::Error, + code: Some("P0041"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "expected expression before `:`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0041_04.uc"), + &ExpectedDiagnostic { + headline: "missing expression after `case`", + severity: Severity::Error, + code: Some("P0041"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "expected expression before `:`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "after this `case`, an expression was expected", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0041_05.uc"), + &ExpectedDiagnostic { + headline: "missing expression after `case`", + severity: Severity::Error, + code: Some("P0041"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(11), + end: TokenPosition(11), + }, + message: "expected expression before `:`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(8), + end: TokenPosition(8), + }, + message: "after this `case`, an expression was expected", + }, + ], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0042_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0042_01.uc", + source: "switch(A) {\n case *:\n}\n", + }, + Fixture { + label: "files/P0042_02.uc", + source: "switch\n(A)\n{\n case\n =\n :\n}\n", + }, + Fixture { + label: "files/P0042_03.uc", + source: "switch(A) {\n case &&:\n default:\n}\n", + }, + Fixture { + label: "files/P0042_04.uc", + source: "switch\n(\n A\n)\n{\n case\n .\n :\n case 1:\n}\n", + }, + Fixture { + label: "files/P0042_05.uc", + source: "switch(A) {\n case ]:\n}\n", + }, +]; + +#[test] +fn check_p0042_fixtures() { + let runs = run_fixtures(P0042_FIXTURES); + + assert_eq!(runs.get("files/P0042_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0042_02.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0042_03.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0042_04.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0042_05.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0042_01.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `*`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `*`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0042_02.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `=`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "unexpected `=`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(9), + end: TokenPosition(9), + }, + message: "after this `case`, an expression was expected", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0042_03.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `&&`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `&&`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0042_04.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `.`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "unexpected `.`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "after this `case`, an expression was expected", + }, + ], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0042_05.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `]`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `]`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); +} + +pub(super) const P0042_CASCADE_FIXTURES: &[Fixture] = &[ + Fixture { + label: "files/P0042_cascade_01.uc", + source: "switch(A) {\n case *\n case 1:\n}\n", + }, + Fixture { + label: "files/P0042_cascade_02.uc", + source: "switch\n(\n A\n)\n{\n case\n =\n default:\n}\n", + }, +]; + +#[test] +fn check_p0042_cascade_fixtures() { + let runs = run_fixtures(P0042_CASCADE_FIXTURES); + + assert_eq!(runs.get("files/P0042_cascade_01.uc").unwrap().len(), 1); + assert_eq!(runs.get("files/P0042_cascade_02.uc").unwrap().len(), 1); + + assert_diagnostic( + &runs.get_any("files/P0042_cascade_01.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `*`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(10), + end: TokenPosition(10), + }, + message: "unexpected `*`", + }), + secondary_labels: &[], + help: None, + notes: &[], + }, + ); + + assert_diagnostic( + &runs.get_any("files/P0042_cascade_02.uc"), + &ExpectedDiagnostic { + headline: "expected expression after `case`, found `=`", + severity: Severity::Error, + code: Some("P0042"), + primary_label: Some(ExpectedLabel { + span: TokenSpan { + start: TokenPosition(15), + end: TokenPosition(15), + }, + message: "unexpected `=`", + }), + secondary_labels: &[ + ExpectedLabel { + span: TokenSpan { + start: TokenPosition(12), + end: TokenPosition(12), + }, + message: "after this `case`, an expression was expected", + }, + ], + help: None, + notes: &[], + }, + ); +}