From 87eaf9a18b47621fb7c2a460fdef779defddb602 Mon Sep 17 00:00:00 2001 From: azjezz Date: Thu, 1 Dec 2022 15:17:23 +0100 Subject: [PATCH] feat: better enum errors Signed-off-by: azjezz --- src/parser/error.rs | 4 + src/parser/internal/classish.rs | 42 +++---- src/parser/internal/classish_statement.rs | 28 ++++- src/parser/internal/functions.rs | 45 +++---- src/parser/macros.rs | 46 ++++--- src/parser/state.rs | 2 +- tests/0087/parser-error.txt | 2 +- tests/0147/code.php | 8 ++ tests/0147/parser-error.txt | 1 + tests/0147/tokens.txt | 142 ++++++++++++++++++++++ tests/0148/code.php | 8 ++ tests/0148/parser-error.txt | 1 + tests/0148/tokens.txt | 126 +++++++++++++++++++ 13 files changed, 386 insertions(+), 69 deletions(-) create mode 100644 tests/0147/code.php create mode 100644 tests/0147/parser-error.txt create mode 100644 tests/0147/tokens.txt create mode 100644 tests/0148/code.php create mode 100644 tests/0148/parser-error.txt create mode 100644 tests/0148/tokens.txt diff --git a/src/parser/error.rs b/src/parser/error.rs index adb963f8..bebb0da6 100644 --- a/src/parser/error.rs +++ b/src/parser/error.rs @@ -19,6 +19,8 @@ pub enum ParseError { PromotedPropertyOnAbstractConstructor(Span), AbstractModifierOnNonAbstractClassMethod(Span), ConstructorInEnum(String, Span), + MissingCaseValueForBackedEnum(String, String, Span), + CaseValueForUnitEnum(String, String, Span), StaticModifierOnConstant(Span), ReadonlyModifierOnConstant(Span), FinalModifierOnAbstractClassMember(Span), @@ -61,6 +63,8 @@ impl Display for ParseError { Self::AbstractModifierOnNonAbstractClassMethod(span) => write!(f, "Parse Error: Cannot declare abstract methods on a non-abstract class on line {} column {}", span.0, span.1), Self::FinalModifierOnAbstractClass(span) => write!(f, "Parse Error: Cannot use the final modifier on an abstract class on line {} column {}", span.0, span.1), Self::ConstructorInEnum(name, span) => write!(f, "Parse Error: Enum '{}' cannot have a constructor on line {} column {}", name, span.0, span.1), + Self::MissingCaseValueForBackedEnum(case, name, span) => write!(f, "Parse Error: Case `{}` of backed enum `{}` must have a value on line {} column {}", case, name, span.0, span.1), + Self::CaseValueForUnitEnum(case, name, span) => write!(f, "Parse Error: Case `{}` of unit enum `{}` must not have a value on line {} column {}", case, name, span.0, span.1), Self::UnpredictableState(span) => write!(f, "Parse Error: Reached an unpredictable state on line {} column {}", span.0, span.1) } } diff --git a/src/parser/internal/classish.rs b/src/parser/internal/classish.rs index 0dd32a28..98402606 100644 --- a/src/parser/internal/classish.rs +++ b/src/parser/internal/classish.rs @@ -11,7 +11,6 @@ use crate::parser::state::State; use crate::parser::Parser; use crate::expect_token; -use crate::expected_token_err; use crate::scoped; impl Parser { @@ -195,28 +194,23 @@ impl Parser { let name = self.ident(state)?; - scoped!(state, Scope::Enum(name.clone()), { - let backed_type: Option = if state.current.kind == TokenKind::Colon { - self.colon(state)?; - - match state.current.kind.clone() { - TokenKind::Identifier(s) if s == b"string" || s == b"int" => { - state.next(); - - Some(match &s[..] { - b"string" => BackedEnumType::String, - b"int" => BackedEnumType::Int, - _ => unreachable!(), - }) - } - _ => { - return expected_token_err!(["`string`", "`int`"], state); - } - } - } else { - None - }; - + let backed_type: Option = if state.current.kind == TokenKind::Colon { + self.colon(state)?; + + expect_token!([ + TokenKind::Identifier(s) if s == b"string" || s == b"int" => { + Some(match &s[..] { + b"string" => BackedEnumType::String, + b"int" => BackedEnumType::Int, + _ => unreachable!(), + }) + }, + ], state, ["`string`", "`int`",]) + } else { + None + }; + + scoped!(state, Scope::Enum(name.clone(), backed_type.is_some()), { let mut implements = Vec::new(); if state.current.kind == TokenKind::Implements { state.next(); @@ -233,7 +227,7 @@ impl Parser { let mut body = Block::new(); while state.current.kind != TokenKind::RightBrace { state.skip_comments(); - body.push(self.enum_statement(state, backed_type.is_some())?); + body.push(self.enum_statement(state)?); } self.rbrace(state)?; diff --git a/src/parser/internal/classish_statement.rs b/src/parser/internal/classish_statement.rs index 7d846ea3..0f6eac1c 100644 --- a/src/parser/internal/classish_statement.rs +++ b/src/parser/internal/classish_statement.rs @@ -1,3 +1,4 @@ +use crate::expected_scope; use crate::lexer::token::TokenKind; use crate::parser::ast::Identifier; use crate::parser::ast::MethodFlag; @@ -6,6 +7,7 @@ use crate::parser::ast::TraitAdaptation; use crate::parser::error::ParseError; use crate::parser::error::ParseResult; use crate::parser::internal::precedence::Precedence; +use crate::parser::state::Scope; use crate::parser::state::State; use crate::parser::Parser; @@ -37,17 +39,25 @@ impl Parser { ], state, ["`const`", "`function`"]) } - pub(in crate::parser) fn enum_statement( - &self, - state: &mut State, - backed: bool, - ) -> ParseResult { + pub(in crate::parser) fn enum_statement(&self, state: &mut State) -> ParseResult { + let (enum_name, backed) = expected_scope!([ + Scope::Enum(enum_name, backed) => (enum_name, backed), + ], state); + if state.current.kind == TokenKind::Case { state.next(); let name = self.ident(state)?; if backed { + if state.current.kind == TokenKind::SemiColon { + return Err(ParseError::MissingCaseValueForBackedEnum( + name.to_string(), + state.named(&enum_name), + state.current.span, + )); + } + expect_token!([TokenKind::Equals], state, "`=`"); let value = self.expression(state, Precedence::Lowest)?; @@ -58,6 +68,14 @@ impl Parser { value, }); } else { + if state.current.kind == TokenKind::Equals { + return Err(ParseError::CaseValueForUnitEnum( + name.to_string(), + state.named(&enum_name), + state.current.span, + )); + } + self.semi(state)?; return Ok(Statement::UnitEnumCase { name: name.into() }); diff --git a/src/parser/internal/functions.rs b/src/parser/internal/functions.rs index 5b07840d..b393726e 100644 --- a/src/parser/internal/functions.rs +++ b/src/parser/internal/functions.rs @@ -1,4 +1,5 @@ use crate::expect_token; +use crate::expected_scope; use crate::lexer::token::TokenKind; use crate::parser::ast::ClassFlag; use crate::parser::ast::ClosureUse; @@ -203,32 +204,32 @@ impl Parser { let name = self.ident_maybe_reserved(state)?; - scoped!(state, Scope::Method(name.clone(), flags.clone()), { - let has_body = match state.parent()? { - Scope::Class(_, cf) => { - if !cf.contains(&ClassFlag::Abstract) && flags.contains(&MethodFlag::Abstract) { - return Err(ParseError::AbstractModifierOnNonAbstractClassMethod( - state.current.span, - )); - } - - !flags.contains(&MethodFlag::Abstract) + let has_body = expected_scope!([ + Scope::Class(_, cf) => { + if !cf.contains(&ClassFlag::Abstract) && flags.contains(&MethodFlag::Abstract) { + return Err(ParseError::AbstractModifierOnNonAbstractClassMethod( + state.current.span, + )); } - Scope::Trait(_) => !flags.contains(&MethodFlag::Abstract), - Scope::Interface(_) => false, - Scope::Enum(enum_name) => { - if name.to_string() == "__construct" { - return Err(ParseError::ConstructorInEnum( - state.named(enum_name), - state.current.span, - )); - } - true + !flags.contains(&MethodFlag::Abstract) + }, + Scope::Trait(_) => !flags.contains(&MethodFlag::Abstract), + Scope::Interface(_) => false, + Scope::Enum(enum_name, _) => { + if name.to_string() == "__construct" { + return Err(ParseError::ConstructorInEnum( + state.named(&enum_name), + state.current.span, + )); } - _ => true, - }; + true + }, + Scope::AnonymousClass => true, + ], state); + + scoped!(state, Scope::Method(name.clone(), flags.clone()), { self.lparen(state)?; let params = self.param_list(state)?; diff --git a/src/parser/macros.rs b/src/parser/macros.rs index e816fcc9..b49e7176 100644 --- a/src/parser/macros.rs +++ b/src/parser/macros.rs @@ -1,43 +1,43 @@ #[macro_export] macro_rules! peek_token { - ([ $($expected:pat => $out:expr),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => {{ + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? => $out:expr),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => {{ $state.skip_comments(); match $state.current.kind.clone() { $( - $expected => $out, + $( $pattern )|+ $( if $guard )? => $out, )+ _ => { return $crate::expected_token_err!([ $($message,)+ ], $state); } } }}; - ([ $($expected:pat),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => {{ + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => {{ $state.skip_comments(); - if !matches!($state.current.kind, $(| $expected )+) { + if !matches!($state.current.kind, $( $pattern )|+ $( if $guard )?) { return $crate::expected_token_err!([ $($message,)+ ], $state); } }}; - ([ $($expected:pat => $out:expr),+ $(,)? ], $state:expr, $message:literal) => { - $crate::peek_token!([ $($expected => $out,)+ ], $state, [$message]) + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? => $out:expr),+ $(,)? ], $state:expr, $message:literal) => { + $crate::peek_token!([ $($( $pattern )|+ $( if $guard )? => $out,)+ ], $state, [$message]) }; - ([ $($expected:pat),+ $(,)? ], $state:expr, $message:literal) => { - $crate::peek_token!([ $($expected,)+ ], $state, [$message]) + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?),+ $(,)? ], $state:expr, $message:literal) => { + $crate::peek_token!([ $($( $pattern )|+ $( if $guard )?,)+ ], $state, [$message]) }; } #[macro_export] macro_rules! expect_token { - ([ $($expected:pat => $out:expr),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => { - $crate::peek_token!([ $($expected => { $state.next(); $out },)+ ], $state, [$($message,)+]) + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? => $out:expr),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => { + $crate::peek_token!([ $($( $pattern )|+ $( if $guard )? => { $state.next(); $out },)+ ], $state, [$($message,)+]) }; - ([ $($expected:pat),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => { - $crate::peek_token!([ $($expected => { $state.next(); },)+ ], $state, [$($message,)+]) + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?),+ $(,)? ], $state:expr, [ $($message:literal),+ $(,)? ]) => { + $crate::peek_token!([ $($( $pattern )|+ $( if $guard )? => { $state.next(); },)+ ], $state, [$($message,)+]) }; - ([ $($expected:pat => $out:expr),+ $(,)? ], $state:expr, $message:literal) => { - $crate::peek_token!([ $($expected => { $state.next(); $out },)+ ], $state, [$message]) + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? => $out:expr),+ $(,)? ], $state:expr, $message:literal) => { + $crate::peek_token!([ $($( $pattern )|+ $( if $guard )? => { $state.next(); $out },)+ ], $state, [$message]) }; - ([ $($expected:pat),+ $(,)? ], $state:expr, $message:literal) => { - $crate::peek_token!([ $($expected => { $state.next(); },)+ ], $state, [$message]) + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?),+ $(,)? ], $state:expr, $message:literal) => { + $crate::peek_token!([ $($( $pattern )|+ $( if $guard )? => { $state.next(); },)+ ], $state, [$message]) }; } @@ -94,6 +94,20 @@ macro_rules! expected_token_err { }; } +#[macro_export] +macro_rules! expected_scope { + ([ $($(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? => $out:expr),+ $(,)? ], $state:expr) => {{ + match $state.scope().cloned()? { + $( + $( $pattern )|+ $( if $guard )? => $out, + )+ + _ => { + return Err($crate::parser::error::ParseError::UnpredictableState($state.current.span)); + } + } + }}; +} + #[macro_export] macro_rules! scoped { ($state:expr, $scope:expr, $block:block) => {{ diff --git a/src/parser/state.rs b/src/parser/state.rs index 173ca892..d9aace41 100644 --- a/src/parser/state.rs +++ b/src/parser/state.rs @@ -17,7 +17,7 @@ pub enum Scope { Interface(ByteString), Class(ByteString, Vec), Trait(ByteString), - Enum(ByteString), + Enum(ByteString, bool), AnonymousClass, Function(ByteString), diff --git a/tests/0087/parser-error.txt b/tests/0087/parser-error.txt index 410594e8..2ac64645 100644 --- a/tests/0087/parser-error.txt +++ b/tests/0087/parser-error.txt @@ -1 +1 @@ -ExpectedToken(["`=`"], Some(";"), (5, 13)) -> Parse Error: unexpected token `;`, expecting `=` on line 5 column 13 +MissingCaseValueForBackedEnum("Bar", "Foo", (5, 13)) -> Parse Error: Case `Bar` of backed enum `Foo` must have a value on line 5 column 13 diff --git a/tests/0147/code.php b/tests/0147/code.php new file mode 100644 index 00000000..b293e450 --- /dev/null +++ b/tests/0147/code.php @@ -0,0 +1,8 @@ + Parse Error: Case `Baz` of backed enum `A\B\C\D\E\Foo` must have a value on line 7 column 14 diff --git a/tests/0147/tokens.txt b/tests/0147/tokens.txt new file mode 100644 index 00000000..88dae872 --- /dev/null +++ b/tests/0147/tokens.txt @@ -0,0 +1,142 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "A\B\C\D\E", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 3, + 20, + ), + }, + Token { + kind: Enum, + span: ( + 5, + 1, + ), + }, + Token { + kind: Identifier( + "Foo", + ), + span: ( + 5, + 6, + ), + }, + Token { + kind: Colon, + span: ( + 5, + 9, + ), + }, + Token { + kind: Identifier( + "int", + ), + span: ( + 5, + 11, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 15, + ), + }, + Token { + kind: Case, + span: ( + 6, + 6, + ), + }, + Token { + kind: Identifier( + "Bar", + ), + span: ( + 6, + 11, + ), + }, + Token { + kind: Equals, + span: ( + 6, + 15, + ), + }, + Token { + kind: LiteralInteger( + 1, + ), + span: ( + 6, + 17, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 18, + ), + }, + Token { + kind: Case, + span: ( + 7, + 6, + ), + }, + Token { + kind: Identifier( + "Baz", + ), + span: ( + 7, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 7, + 14, + ), + }, + Token { + kind: RightBrace, + span: ( + 8, + 1, + ), + }, +] diff --git a/tests/0148/code.php b/tests/0148/code.php new file mode 100644 index 00000000..884ae634 --- /dev/null +++ b/tests/0148/code.php @@ -0,0 +1,8 @@ + Parse Error: Case `Baz` of unit enum `A\B\C\D\E\Foo` must not have a value on line 7 column 15 diff --git a/tests/0148/tokens.txt b/tests/0148/tokens.txt new file mode 100644 index 00000000..562b9e59 --- /dev/null +++ b/tests/0148/tokens.txt @@ -0,0 +1,126 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "A\B\C\D\E", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 3, + 20, + ), + }, + Token { + kind: Enum, + span: ( + 5, + 1, + ), + }, + Token { + kind: Identifier( + "Foo", + ), + span: ( + 5, + 6, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 10, + ), + }, + Token { + kind: Case, + span: ( + 6, + 6, + ), + }, + Token { + kind: Identifier( + "Bar", + ), + span: ( + 6, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 14, + ), + }, + Token { + kind: Case, + span: ( + 7, + 6, + ), + }, + Token { + kind: Identifier( + "Baz", + ), + span: ( + 7, + 11, + ), + }, + Token { + kind: Equals, + span: ( + 7, + 15, + ), + }, + Token { + kind: LiteralInteger( + 2, + ), + span: ( + 7, + 17, + ), + }, + Token { + kind: SemiColon, + span: ( + 7, + 18, + ), + }, + Token { + kind: RightBrace, + span: ( + 8, + 1, + ), + }, +]