From 0f42efd2b732d62859317aea9f65c9f9d1d36d86 Mon Sep 17 00:00:00 2001 From: azjezz Date: Fri, 2 Dec 2022 16:48:35 +0100 Subject: [PATCH] chore: improve properties parser Signed-off-by: azjezz --- src/lexer/token.rs | 6 +- src/parser/ast.rs | 30 +++ src/parser/error.rs | 10 +- src/parser/internal/classish_statement.rs | 101 +++---- src/parser/internal/functions.rs | 8 +- src/parser/internal/ident.rs | 25 -- src/parser/internal/params.rs | 32 ++- src/parser/internal/types.rs | 312 ++++++++++++++++++---- src/parser/internal/vars.rs | 18 +- src/parser/macros.rs | 21 +- tests/0004/parser-error.txt | 2 +- tests/0044/parser-error.txt | 2 +- tests/0111/parser-error.txt | 2 +- tests/0112/parser-error.txt | 2 +- tests/0124/parser-error.txt | 2 +- tests/0128/parser-error.txt | 2 +- tests/0149/ast.txt | 49 ++++ tests/0149/code.php | 7 + tests/0149/tokens.txt | 151 +++++++++++ tests/0150/ast.txt | 49 ++++ tests/0150/code.php | 7 + tests/0150/tokens.txt | 149 +++++++++++ tests/0151/ast.txt | 49 ++++ tests/0151/code.php | 7 + tests/0151/tokens.txt | 151 +++++++++++ tests/0152/code.php | 9 + tests/0152/parser-error.txt | 1 + tests/0152/tokens.txt | 152 +++++++++++ tests/0153/ast.txt | 52 ++++ tests/0153/code.php | 9 + tests/0153/tokens.txt | 177 ++++++++++++ tests/0154/code.php | 7 + tests/0154/parser-error.txt | 1 + tests/0154/tokens.txt | 117 ++++++++ tests/0155/ast.txt | 31 +++ tests/0155/code.php | 7 + tests/0155/tokens.txt | 110 ++++++++ tests/0156/code.php | 7 + tests/0156/parser-error.txt | 1 + tests/0156/tokens.txt | 126 +++++++++ 40 files changed, 1828 insertions(+), 173 deletions(-) create mode 100644 tests/0149/ast.txt create mode 100644 tests/0149/code.php create mode 100644 tests/0149/tokens.txt create mode 100644 tests/0150/ast.txt create mode 100644 tests/0150/code.php create mode 100644 tests/0150/tokens.txt create mode 100644 tests/0151/ast.txt create mode 100644 tests/0151/code.php create mode 100644 tests/0151/tokens.txt create mode 100644 tests/0152/code.php create mode 100644 tests/0152/parser-error.txt create mode 100644 tests/0152/tokens.txt create mode 100644 tests/0153/ast.txt create mode 100644 tests/0153/code.php create mode 100644 tests/0153/tokens.txt create mode 100644 tests/0154/code.php create mode 100644 tests/0154/parser-error.txt create mode 100644 tests/0154/tokens.txt create mode 100644 tests/0155/ast.txt create mode 100644 tests/0155/code.php create mode 100644 tests/0155/tokens.txt create mode 100644 tests/0156/code.php create mode 100644 tests/0156/parser-error.txt create mode 100644 tests/0156/tokens.txt diff --git a/src/lexer/token.rs b/src/lexer/token.rs index fe55a8c1..026c6644 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -282,7 +282,7 @@ impl Display for TokenKind { Self::EndSwitch => "endswitch", Self::EndWhile => "endwhile", Self::Enum => "enum", - Self::Eof => "", + Self::Eof => "[end of file]", Self::Equals => "=", Self::Extends => "extends", Self::False => "false", @@ -382,8 +382,10 @@ impl Display for TokenKind { Self::Interface => "interface", Self::NamespaceConstant => "__NAMESPACE__", Self::PowEquals => "**=", + Self::Variable(v) => { + return write!(f, "${}", v); + } Self::StringPart(v) - | Self::Variable(v) | Self::QualifiedIdentifier(v) | Self::Identifier(v) | Self::FullyQualifiedIdentifier(v) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 3c53862d..f52d8ca9 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -32,12 +32,39 @@ pub enum Type { Mixed, Callable, Iterable, + StaticReference, + SelfReference, + ParentReference, } impl Type { pub fn standalone(&self) -> bool { matches!(self, Type::Mixed | Type::Never | Type::Void) } + + pub fn nullable(&self) -> bool { + matches!(self, Type::Nullable(_)) + } + + pub fn includes_callable(&self) -> bool { + match &self { + Self::Callable => true, + Self::Union(types) | Self::Intersection(types) => { + types.iter().any(|x| x.includes_callable()) + } + _ => false, + } + } + + pub fn includes_class_scoped(&self) -> bool { + match &self { + Self::StaticReference | Self::SelfReference | Self::ParentReference => true, + Self::Union(types) | Self::Intersection(types) => { + types.iter().any(|x| x.includes_class_scoped()) + } + _ => false, + } + } } impl Display for Type { @@ -77,6 +104,9 @@ impl Display for Type { Type::Mixed => write!(f, "mixed"), Type::Callable => write!(f, "callable"), Type::Iterable => write!(f, "iterable"), + Type::StaticReference => write!(f, "static"), + Type::SelfReference => write!(f, "self"), + Type::ParentReference => write!(f, "parent"), } } } diff --git a/src/parser/error.rs b/src/parser/error.rs index bebb0da6..870ca937 100644 --- a/src/parser/error.rs +++ b/src/parser/error.rs @@ -15,6 +15,7 @@ pub enum ParseError { StandaloneTypeUsedInCombination(Type, Span), TryWithoutCatchOrFinally(Span), VariadicPromotedProperty(Span), + MissingTypeForReadonlyProperty(String, String, Span), PromotedPropertyOutsideConstructor(Span), PromotedPropertyOnAbstractConstructor(Span), AbstractModifierOnNonAbstractClassMethod(Span), @@ -27,6 +28,8 @@ pub enum ParseError { FinalModifierOnPrivateConstant(Span), FinalModifierOnAbstractClass(Span), UnpredictableState(Span), + StaticPropertyUsingReadonlyModifier(String, String, Span), + ReadonlyPropertyHasDefaultValue(String, String, Span), } impl Display for ParseError { @@ -47,11 +50,12 @@ impl Display for ParseError { None => write!(f, "Parse Error: unexpected end of file, expecting {} on line {} column {}", expected, span.0, span.1), } }, + Self::MissingTypeForReadonlyProperty(class, prop, span) => write!(f, "Parse Error: Readonly property {}::${} must have type on line {} column {}", class, prop, span.0, span.1), Self::MultipleModifiers(modifier, span) => write!(f, "Parse Error: Multiple {} modifiers are not allowed on line {} column {}", modifier, span.0, span.1), Self::MultipleAccessModifiers( span) => write!(f, "Parse Error: Multiple access type modifiers are not allowed on line {} column {}", span.0, span.1), Self::UnexpectedToken(message, span) => write!(f, "Parse Error: Unexpected token {} on line {} column {}", message, span.0, span.1), Self::UnexpectedEndOfFile => write!(f, "Parse Error: unexpected end of file."), - Self::FinalModifierOnAbstractClassMember(span) => write!(f, "Parse Error: Cannot use the final modifier on an abstract class member on line {} column {}", span.0, span.1), + Self::FinalModifierOnAbstractClassMember(span) => write!(f, "Parse Error: Cannot use 'final' as an abstract class member modifier on line {} column {}", span.0, span.1), Self::StaticModifierOnConstant(span) => write!(f, "Parse Error: Cannot use 'static' as constant modifier on line {} column {}", span.0, span.1), Self::ReadonlyModifierOnConstant(span) => write!(f, "Parse Error: Cannot use 'readonly' as constant modifier on line {} column {}", span.0, span.1), Self::FinalModifierOnPrivateConstant(span) => write!(f, "Parse Error: Private constant cannot be final as it is not visible to other classes on line {} column {}", span.0, span.1), @@ -65,7 +69,9 @@ impl Display for ParseError { 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) + Self::UnpredictableState(span) => write!(f, "Parse Error: Reached an unpredictable state on line {} column {}", span.0, span.1), + Self::StaticPropertyUsingReadonlyModifier(class, prop, span) => write!(f, "Parse Error: Static property {}:${} cannot be readonly on line {} column {}", class, prop, span.0, span.1), + Self::ReadonlyPropertyHasDefaultValue(class, prop, span) => write!(f, "Parse Error: Readonly property {}:${} cannot have a default value on line {} column {}", class, prop, span.0, span.1), } } } diff --git a/src/parser/internal/classish_statement.rs b/src/parser/internal/classish_statement.rs index 0f6eac1c..cd5985a2 100644 --- a/src/parser/internal/classish_statement.rs +++ b/src/parser/internal/classish_statement.rs @@ -2,6 +2,7 @@ use crate::expected_scope; use crate::lexer::token::TokenKind; use crate::parser::ast::Identifier; use crate::parser::ast::MethodFlag; +use crate::parser::ast::PropertyFlag; use crate::parser::ast::Statement; use crate::parser::ast::TraitAdaptation; use crate::parser::error::ParseError; @@ -12,7 +13,6 @@ use crate::parser::state::State; use crate::parser::Parser; use crate::expect_token; -use crate::expected_token_err; use crate::peek_token; impl Parser { @@ -123,15 +123,22 @@ impl Parser { let member_flags = self.class_members_flags(state)?; - match &state.current.kind { - TokenKind::Const => self.parse_classish_const(state, member_flags), - TokenKind::Function => self.method( + if state.current.kind == TokenKind::Const { + return self.parse_classish_const(state, member_flags); + } + + if state.current.kind == TokenKind::Function { + return self.method( state, member_flags.iter().map(|t| t.clone().into()).collect(), - ), - // TODO - TokenKind::Variable(_) => { - let var = self.var(state)?; + ); + } + + let ty = self.get_optional_type(state)?; + + expect_token!([ + TokenKind::Variable(var) => { + let flags: Vec = member_flags.into_iter().map(|f| f.into()).collect(); let mut value = None; if state.current.kind == TokenKind::Equals { @@ -141,56 +148,42 @@ impl Parser { self.semi(state)?; - Ok(Statement::Property { - var, - value, - r#type: None, - flags: member_flags.into_iter().map(|f| f.into()).collect(), - }) - } - TokenKind::Question - | TokenKind::Identifier(_) - | TokenKind::QualifiedIdentifier(_) - | TokenKind::FullyQualifiedIdentifier(_) - | TokenKind::Array - | TokenKind::Null => { - let prop_type = self.type_string(state)?; - let var = self.var(state)?; - let mut value = None; + if flags.contains(&PropertyFlag::Readonly) { + if flags.contains(&PropertyFlag::Static) { + let class_name: String = expected_scope!([ + Scope::Class(name, _) => state.named(&name), + Scope::Trait(name) => state.named(&name), + Scope::AnonymousClass => state.named(&"class@anonymous".into()), + ], state); - if state.current.kind == TokenKind::Equals { - state.next(); - value = Some(self.expression(state, Precedence::Lowest)?); - } + return Err(ParseError::StaticPropertyUsingReadonlyModifier(class_name, var.to_string(), state.current.span)); + } - // TODO: Support comma-separated property declarations. - // nikic/php-parser does this with a single Property statement - // that is capable of holding multiple property declarations. - self.semi(state)?; + if value.is_some() { + let class_name: String = expected_scope!([ + Scope::Class(name, _) => state.named(&name), + Scope::Trait(name) => state.named(&name), + Scope::AnonymousClass => state.named(&"class@anonymous".into()), + ], state); + + return Err(ParseError::ReadonlyPropertyHasDefaultValue(class_name, var.to_string(), state.current.span)); + } + } Ok(Statement::Property { var, value, - r#type: Some(prop_type), - flags: member_flags.into_iter().map(|f| f.into()).collect(), + r#type: ty, + flags, }) } - _ => expected_token_err!( - ["`const`", "`function`", "an identifier", "a varaible"], - state - ), - } + ], state, ["a varaible"]) } fn parse_classish_var(&self, state: &mut State) -> ParseResult { state.next(); - let mut var_type = None; - - if !matches!(state.current.kind, TokenKind::Variable(_)) { - var_type = Some(self.type_string(state)?); - } - + let ty = self.get_optional_type(state)?; let var = self.var(state)?; let mut value = None; @@ -205,7 +198,7 @@ impl Parser { Ok(Statement::Var { var, value, - r#type: var_type, + r#type: ty, }) } @@ -238,10 +231,8 @@ impl Parser { _ => (None, self.ident(state)?.into()), }; - match state.current.kind { + expect_token!([ TokenKind::As => { - state.next(); - match state.current.kind { TokenKind::Public | TokenKind::Protected | TokenKind::Private => { let visibility: MethodFlag = state.current.kind.clone().into(); @@ -273,10 +264,8 @@ impl Parser { }); } } - } + }, TokenKind::Insteadof => { - state.next(); - let mut insteadof = Vec::new(); insteadof.push(self.full_name(state)?.into()); while state.current.kind != TokenKind::SemiColon { @@ -290,13 +279,7 @@ impl Parser { insteadof, }); } - _ => { - return Err(ParseError::UnexpectedToken( - state.current.kind.to_string(), - state.current.span, - )) - } - }; + ], state, ["`as`", "`insteadof`"]); self.semi(state)?; } diff --git a/src/parser/internal/functions.rs b/src/parser/internal/functions.rs index b393726e..186f8fbb 100644 --- a/src/parser/internal/functions.rs +++ b/src/parser/internal/functions.rs @@ -79,7 +79,7 @@ impl Parser { if state.current.kind == TokenKind::Colon { self.colon(state)?; - return_type = Some(self.type_string(state)?); + return_type = Some(self.get_type(state)?); } self.lbrace(state)?; @@ -128,7 +128,7 @@ impl Parser { if state.current.kind == TokenKind::Colon { self.colon(state)?; - return_type = Some(self.type_string(state)?); + return_type = Some(self.get_type(state)?); } expect_token!([TokenKind::DoubleArrow], state, ["`=>`"]); @@ -169,7 +169,7 @@ impl Parser { if state.current.kind == TokenKind::Colon { self.colon(state)?; - return_type = Some(self.type_string(state)?); + return_type = Some(self.get_type(state)?); } self.lbrace(state)?; @@ -241,7 +241,7 @@ impl Parser { if state.current.kind == TokenKind::Colon { self.colon(state)?; - return_type = Some(self.type_string(state)?); + return_type = Some(self.get_type(state)?); } if !has_body { diff --git a/src/parser/internal/ident.rs b/src/parser/internal/ident.rs index 3000caf1..c5c8faf1 100644 --- a/src/parser/internal/ident.rs +++ b/src/parser/internal/ident.rs @@ -37,31 +37,6 @@ impl Parser { ], state, "a variable")) } - pub(in crate::parser) fn full_name_maybe_type_keyword( - &self, - state: &mut State, - ) -> ParseResult { - match state.current.kind { - TokenKind::Array | TokenKind::Callable => { - let r = Ok(state.current.kind.to_string().into()); - state.next(); - r - } - _ => self.full_name(state), - } - } - - pub(in crate::parser) fn type_with_static(&self, state: &mut State) -> ParseResult { - Ok(match state.current.kind { - TokenKind::Static | TokenKind::Null | TokenKind::True | TokenKind::False => { - let str = state.current.kind.to_string(); - state.next(); - str.into() - } - _ => self.full_name_maybe_type_keyword(state)?, - }) - } - pub(in crate::parser) fn ident_maybe_reserved( &self, state: &mut State, diff --git a/src/parser/internal/params.rs b/src/parser/internal/params.rs index d62a9da6..ec59c1b4 100644 --- a/src/parser/internal/params.rs +++ b/src/parser/internal/params.rs @@ -18,6 +18,7 @@ impl Parser { pub(in crate::parser) fn param_list(&self, state: &mut State) -> Result { let mut params = ParamList::new(); + let mut class_name = String::new(); let construct: i8 = match state.scope()? { Scope::Function(_) | Scope::AnonymousFunction(_) | Scope::ArrowFunction(_) => 0, Scope::Method(name, flags) => { @@ -28,13 +29,19 @@ impl Parser { // can only have abstract ctor Scope::Interface(_) => 1, // can only have concret ctor - Scope::AnonymousClass => 2, + Scope::AnonymousClass => { + class_name = state.named(&"class@anonymous".into()); + + 2 + } // can have either abstract or concret ctor, // depens on method flag. - Scope::Class(_, _) | Scope::Trait(_) => { + Scope::Class(name, _) | Scope::Trait(name) => { if flags.contains(&MethodFlag::Abstract) { 1 } else { + class_name = state.named(name); + 2 } } @@ -70,15 +77,12 @@ impl Parser { } } - // If this is a readonly promoted property, or we don't see a variable - if flags.contains(&PropertyFlag::Readonly) - || !matches!( - state.current.kind, - TokenKind::Variable(_) | TokenKind::Ellipsis | TokenKind::Ampersand - ) - { + if !matches!( + state.current.kind, + TokenKind::Variable(_) | TokenKind::Ellipsis | TokenKind::Ampersand + ) { // Try to parse the type. - param_type = Some(self.type_string(state)?); + param_type = Some(self.get_type(state)?); } let mut variadic = false; @@ -103,6 +107,14 @@ impl Parser { TokenKind::Variable(v) => v ], state, "a varaible"); + if flags.contains(&PropertyFlag::Readonly) && param_type.is_none() { + return Err(ParseError::MissingTypeForReadonlyProperty( + class_name, + var.to_string(), + state.current.span, + )); + } + let mut default = None; if state.current.kind == TokenKind::Equals { state.next(); diff --git a/src/parser/internal/types.rs b/src/parser/internal/types.rs index 68d82262..d4071e02 100644 --- a/src/parser/internal/types.rs +++ b/src/parser/internal/types.rs @@ -1,4 +1,4 @@ -use crate::lexer::byte_string::ByteString; +use crate::expected_token; use crate::lexer::token::TokenKind; use crate::parser::ast::TryBlockCaughtType; use crate::parser::ast::Type; @@ -36,39 +36,36 @@ impl Parser { Ok(TryBlockCaughtType::Identifier(id.into())) } - pub(in crate::parser) fn type_string(&self, state: &mut State) -> ParseResult { - if state.current.kind == TokenKind::Question { - state.next(); - let t = self.type_with_static(state)?; - return Ok(Type::Nullable(Box::new(parse_simple_type(t)))); - } + pub(in crate::parser) fn get_type(&self, state: &mut State) -> ParseResult { + let ty = self.maybe_nullable(state, &|state| { + self.maybe_static(state, &|state| self.get_simple_type(state)) + })?; - let id = self.type_with_static(state)?; + if ty.nullable() { + return Ok(ty); + } if state.current.kind == TokenKind::Pipe { state.next(); - let r#type = parse_simple_type(id); - if r#type.standalone() { + if ty.standalone() { return Err(ParseError::StandaloneTypeUsedInCombination( - r#type, + ty, state.current.span, )); } - let mut types = vec![r#type]; - + let mut types = vec![ty]; while !state.is_eof() { - let id = self.type_with_static(state)?; - let r#type = parse_simple_type(id); - if r#type.standalone() { + let ty = self.maybe_static(state, &|state| self.get_simple_type(state))?; + if ty.standalone() { return Err(ParseError::StandaloneTypeUsedInCombination( - r#type, + ty, state.current.span, )); } - types.push(r#type); + types.push(ty); if state.current.kind != TokenKind::Pipe { break; @@ -85,27 +82,24 @@ impl Parser { { state.next(); - let r#type = parse_simple_type(id); - if r#type.standalone() { + if ty.standalone() { return Err(ParseError::StandaloneTypeUsedInCombination( - r#type, + ty, state.current.span, )); } - let mut types = vec![r#type]; - + let mut types = vec![ty]; while !state.is_eof() { - let id = self.type_with_static(state)?; - let r#type = parse_simple_type(id); - if r#type.standalone() { + let ty = self.maybe_static(state, &|state| self.get_simple_type(state))?; + if ty.standalone() { return Err(ParseError::StandaloneTypeUsedInCombination( - r#type, + ty, state.current.span, )); } - types.push(r#type); + types.push(ty); if state.current.kind != TokenKind::Ampersand { break; @@ -117,28 +111,248 @@ impl Parser { return Ok(Type::Intersection(types)); } - Ok(parse_simple_type(id)) + Ok(ty) } -} -fn parse_simple_type(id: ByteString) -> Type { - let name = &id[..]; - let lowered_name = name.to_ascii_lowercase(); - match lowered_name.as_slice() { - b"void" => Type::Void, - b"never" => Type::Never, - b"null" => Type::Null, - b"true" => Type::True, - b"false" => Type::False, - b"float" => Type::Float, - b"bool" => Type::Boolean, - b"int" => Type::Integer, - b"string" => Type::String, - b"array" => Type::Array, - b"object" => Type::Object, - b"mixed" => Type::Mixed, - b"iterable" => Type::Iterable, - b"callable" => Type::Callable, - _ => Type::Identifier(id.into()), + pub(in crate::parser) fn get_optional_type( + &self, + state: &mut State, + ) -> ParseResult> { + let ty = self.maybe_optional_nullable(state, &|state| { + self.maybe_optional_static(state, &|state| self.get_optional_simple_type(state)) + }); + + match ty { + Some(ty) => { + if ty.nullable() { + return Ok(Some(ty)); + } + + if state.current.kind == TokenKind::Pipe { + state.next(); + + if ty.standalone() { + return Err(ParseError::StandaloneTypeUsedInCombination( + ty, + state.current.span, + )); + } + + let mut types = vec![ty]; + while !state.is_eof() { + let ty = self.maybe_static(state, &|state| self.get_simple_type(state))?; + if ty.standalone() { + return Err(ParseError::StandaloneTypeUsedInCombination( + ty, + state.current.span, + )); + } + + types.push(ty); + + if state.current.kind != TokenKind::Pipe { + break; + } else { + state.next(); + } + } + + return Ok(Some(Type::Union(types))); + } + + if state.current.kind == TokenKind::Ampersand + && !matches!(state.peek.kind, TokenKind::Variable(_)) + { + state.next(); + + if ty.standalone() { + return Err(ParseError::StandaloneTypeUsedInCombination( + ty, + state.current.span, + )); + } + + let mut types = vec![ty]; + while !state.is_eof() { + let ty = self.maybe_static(state, &|state| self.get_simple_type(state))?; + if ty.standalone() { + return Err(ParseError::StandaloneTypeUsedInCombination( + ty, + state.current.span, + )); + } + + types.push(ty); + + if state.current.kind != TokenKind::Ampersand { + break; + } else { + state.next(); + } + } + + return Ok(Some(Type::Intersection(types))); + } + + Ok(Some(ty)) + } + None => Ok(None), + } + } + + fn get_optional_simple_type(&self, state: &mut State) -> Option { + match state.current.kind.clone() { + TokenKind::Array => { + state.next(); + + Some(Type::Array) + } + TokenKind::Callable => { + state.next(); + + Some(Type::Callable) + } + TokenKind::Null => { + state.next(); + + Some(Type::Null) + } + TokenKind::True => { + state.next(); + + Some(Type::True) + } + TokenKind::False => { + state.next(); + + Some(Type::False) + } + TokenKind::Identifier(id) => { + state.next(); + + let name = &id[..]; + let lowered_name = name.to_ascii_lowercase(); + match lowered_name.as_slice() { + b"void" => Some(Type::Void), + b"never" => Some(Type::Never), + b"float" => Some(Type::Float), + b"bool" => Some(Type::Boolean), + b"int" => Some(Type::Integer), + b"string" => Some(Type::String), + b"object" => Some(Type::Object), + b"mixed" => Some(Type::Mixed), + b"iterable" => Some(Type::Iterable), + b"null" => Some(Type::Null), + b"true" => Some(Type::True), + b"false" => Some(Type::False), + b"array" => Some(Type::Array), + b"callable" => Some(Type::Callable), + _ => Some(Type::Identifier(id.into())), + } + } + TokenKind::QualifiedIdentifier(id) | TokenKind::FullyQualifiedIdentifier(id) => { + state.next(); + + Some(Type::Identifier(id.into())) + } + _ => None, + } + } + + fn get_simple_type(&self, state: &mut State) -> ParseResult { + self.get_optional_simple_type(state) + .ok_or_else(|| expected_token!(["a type"], state)) + } + + fn maybe_nullable( + &self, + state: &mut State, + otherwise: &(dyn Fn(&mut State) -> ParseResult), + ) -> ParseResult { + if state.current.kind == TokenKind::Question { + state.next(); + + Ok(Type::Nullable(Box::new(otherwise(state)?))) + } else { + otherwise(state) + } + } + + fn maybe_static( + &self, + state: &mut State, + otherwise: &(dyn Fn(&mut State) -> ParseResult), + ) -> ParseResult { + if TokenKind::Static == state.current.kind { + state.next(); + + return Ok(Type::StaticReference); + } + + if let TokenKind::Identifier(id) = &state.current.kind { + let name = &id[..]; + let lowered_name = name.to_ascii_lowercase(); + match lowered_name.as_slice() { + b"self" => { + state.next(); + + return Ok(Type::SelfReference); + } + b"parent" => { + state.next(); + + return Ok(Type::ParentReference); + } + _ => {} + }; + } + + otherwise(state) + } + + fn maybe_optional_nullable( + &self, + state: &mut State, + otherwise: &(dyn Fn(&mut State) -> Option), + ) -> Option { + if state.current.kind == TokenKind::Question { + state.next(); + + Some(Type::Nullable(Box::new(otherwise(state)?))) + } else { + otherwise(state) + } + } + + fn maybe_optional_static( + &self, + state: &mut State, + otherwise: &(dyn Fn(&mut State) -> Option), + ) -> Option { + if TokenKind::Static == state.current.kind { + state.next(); + + return Some(Type::StaticReference); + } + + if let TokenKind::Identifier(id) = &state.current.kind { + let name = &id[..]; + let lowered_name = name.to_ascii_lowercase(); + match lowered_name.as_slice() { + b"self" => { + state.next(); + + return Some(Type::SelfReference); + } + b"parent" => { + state.next(); + + return Some(Type::ParentReference); + } + _ => {} + }; + } + + otherwise(state) } } diff --git a/src/parser/internal/vars.rs b/src/parser/internal/vars.rs index 19920ca5..976f9e7b 100644 --- a/src/parser/internal/vars.rs +++ b/src/parser/internal/vars.rs @@ -1,16 +1,16 @@ use crate::lexer::token::TokenKind; use crate::parser::ast::Expression; -use crate::parser::error::ParseError; use crate::parser::error::ParseResult; use crate::parser::internal::precedence::Precedence; use crate::parser::state::State; use crate::parser::Parser; +use crate::peek_token; impl Parser { pub(in crate::parser) fn dynamic_variable(&self, state: &mut State) -> ParseResult { state.next(); - Ok(match &state.current.kind { + let expr = peek_token!([ TokenKind::LeftBrace => { state.next(); @@ -21,9 +21,9 @@ impl Parser { Expression::DynamicVariable { name: Box::new(name), } - } + }, TokenKind::Variable(variable) => { - let variable = variable.clone(); + let variable = variable; state.next(); @@ -31,12 +31,8 @@ impl Parser { name: Box::new(Expression::Variable { name: variable }), } } - _ => { - return Err(ParseError::UnexpectedToken( - state.current.kind.to_string(), - state.current.span, - )) - } - }) + ], state, ["`{`", "a variable"]); + + Ok(expr) } } diff --git a/src/parser/macros.rs b/src/parser/macros.rs index b49e7176..23041926 100644 --- a/src/parser/macros.rs +++ b/src/parser/macros.rs @@ -70,27 +70,38 @@ macro_rules! expect_literal { #[macro_export] macro_rules! expected_token_err { + ([ $($expected:literal),+ $(,)? ], $state:expr $(,)?) => {{ + Err($crate::expected_token!([$($expected),+], $state)) + }}; + + ($expected:literal, $state:expr $(,)?) => { + $crate::expected_token_err!([$expected], $state) + }; +} + +#[macro_export] +macro_rules! expected_token { ([ $($expected:literal),+ $(,)? ], $state:expr $(,)?) => {{ match &$state.current.kind { TokenKind::Eof => { - Err($crate::parser::error::ParseError::ExpectedToken( + $crate::parser::error::ParseError::ExpectedToken( vec![$($expected.into()),+], None, $state.current.span, - )) + ) }, _ => { - Err($crate::parser::error::ParseError::ExpectedToken( + $crate::parser::error::ParseError::ExpectedToken( vec![$($expected.into()),+], Some($state.current.kind.to_string()), $state.current.span, - )) + ) } } }}; ($expected:literal, $state:expr $(,)?) => { - $crate::expected_token_err!([$expected], $state) + $crate::expected_token!([$expected], $state) }; } diff --git a/tests/0004/parser-error.txt b/tests/0004/parser-error.txt index 242969bd..c0e3cf84 100644 --- a/tests/0004/parser-error.txt +++ b/tests/0004/parser-error.txt @@ -1 +1 @@ -ExpectedToken(["an identifier"], Some("e"), (6, 14)) -> Parse Error: unexpected token `e`, expecting an identifier on line 6 column 14 +ExpectedToken(["an identifier"], Some("$e"), (6, 14)) -> Parse Error: unexpected token `$e`, expecting an identifier on line 6 column 14 diff --git a/tests/0044/parser-error.txt b/tests/0044/parser-error.txt index 950193f0..83ebbfe2 100644 --- a/tests/0044/parser-error.txt +++ b/tests/0044/parser-error.txt @@ -1 +1 @@ -ExpectedToken(["`const`", "`function`", "an identifier", "a varaible"], Some("fn"), (1, 26)) -> Parse Error: unexpected token `fn`, expecting `const`, `function`, an identifier, or a varaible on line 1 column 26 +ExpectedToken(["a varaible"], Some("fn"), (1, 26)) -> Parse Error: unexpected token `fn`, expecting a varaible on line 1 column 26 diff --git a/tests/0111/parser-error.txt b/tests/0111/parser-error.txt index 5369330f..3b913366 100644 --- a/tests/0111/parser-error.txt +++ b/tests/0111/parser-error.txt @@ -1 +1 @@ -ExpectedToken(["an identifier"], Some("..."), (5, 25)) -> Parse Error: unexpected token `...`, expecting an identifier on line 5 column 25 +VariadicPromotedProperty((5, 28)) -> Parse Error: Cannot declare variadic promoted property on line 5 column 28 diff --git a/tests/0112/parser-error.txt b/tests/0112/parser-error.txt index 872d6967..1e796a73 100644 --- a/tests/0112/parser-error.txt +++ b/tests/0112/parser-error.txt @@ -1 +1 @@ -ExpectedToken(["an identifier"], Some("&"), (5, 25)) -> Parse Error: unexpected token `&`, expecting an identifier on line 5 column 25 +MissingTypeForReadonlyProperty("foo", "e", (5, 28)) -> Parse Error: Readonly property foo::$e must have type on line 5 column 28 diff --git a/tests/0124/parser-error.txt b/tests/0124/parser-error.txt index a3fdb76c..2f3898af 100644 --- a/tests/0124/parser-error.txt +++ b/tests/0124/parser-error.txt @@ -1 +1 @@ -FinalModifierOnAbstractClassMember((4, 11)) -> Parse Error: Cannot use the final modifier on an abstract class member on line 4 column 11 +FinalModifierOnAbstractClassMember((4, 11)) -> Parse Error: Cannot use 'final' as an abstract class member modifier on line 4 column 11 diff --git a/tests/0128/parser-error.txt b/tests/0128/parser-error.txt index c9c94267..53dc2568 100644 --- a/tests/0128/parser-error.txt +++ b/tests/0128/parser-error.txt @@ -1 +1 @@ -ExpectedToken(["an identifier"], Some("s"), (5, 25)) -> Parse Error: unexpected token `s`, expecting an identifier on line 5 column 25 +MissingTypeForReadonlyProperty("foo", "s", (5, 28)) -> Parse Error: Readonly property foo::$s must have type on line 5 column 28 diff --git a/tests/0149/ast.txt b/tests/0149/ast.txt new file mode 100644 index 00000000..9a41f989 --- /dev/null +++ b/tests/0149/ast.txt @@ -0,0 +1,49 @@ +[ + Namespace { + name: "A\B\C\D\E", + body: [ + Noop, + Function { + name: Identifier { + name: "foo", + }, + params: [ + Param { + name: Variable { + name: "s", + }, + type: Some( + String, + ), + variadic: false, + default: None, + flags: [], + by_ref: false, + }, + ], + body: [ + Expression { + expr: Call { + target: Identifier { + name: "exit", + }, + args: [ + Arg { + name: None, + value: LiteralInteger { + i: 0, + }, + unpack: false, + }, + ], + }, + }, + ], + return_type: Some( + SelfReference, + ), + by_ref: false, + }, + ], + }, +] diff --git a/tests/0149/code.php b/tests/0149/code.php new file mode 100644 index 00000000..c93b8784 --- /dev/null +++ b/tests/0149/code.php @@ -0,0 +1,7 @@ + Parse Error: Readonly property Foo\Bar\Baz::$name must have type on line 7 column 32 diff --git a/tests/0152/tokens.txt b/tests/0152/tokens.txt new file mode 100644 index 00000000..b2fb3286 --- /dev/null +++ b/tests/0152/tokens.txt @@ -0,0 +1,152 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Bar", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 3, + 18, + ), + }, + Token { + kind: Final, + span: ( + 5, + 1, + ), + }, + Token { + kind: Class, + span: ( + 5, + 7, + ), + }, + Token { + kind: Identifier( + "Baz", + ), + span: ( + 5, + 13, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 17, + ), + }, + Token { + kind: Public, + span: ( + 6, + 6, + ), + }, + Token { + kind: Function, + span: ( + 6, + 13, + ), + }, + Token { + kind: Identifier( + "__construct", + ), + span: ( + 6, + 22, + ), + }, + Token { + kind: LeftParen, + span: ( + 6, + 33, + ), + }, + Token { + kind: Public, + span: ( + 7, + 11, + ), + }, + Token { + kind: Readonly, + span: ( + 7, + 18, + ), + }, + Token { + kind: Variable( + "name", + ), + span: ( + 7, + 27, + ), + }, + Token { + kind: Comma, + span: ( + 7, + 32, + ), + }, + Token { + kind: RightParen, + span: ( + 8, + 6, + ), + }, + Token { + kind: LeftBrace, + span: ( + 8, + 8, + ), + }, + Token { + kind: RightBrace, + span: ( + 8, + 9, + ), + }, + Token { + kind: RightBrace, + span: ( + 9, + 1, + ), + }, +] diff --git a/tests/0153/ast.txt b/tests/0153/ast.txt new file mode 100644 index 00000000..3505def4 --- /dev/null +++ b/tests/0153/ast.txt @@ -0,0 +1,52 @@ +[ + Namespace { + name: "Foo\Bar", + body: [ + Noop, + Class { + name: Identifier { + name: "Baz", + }, + extends: None, + implements: [], + body: [ + Method { + name: Identifier { + name: "__construct", + }, + params: [ + Param { + name: Variable { + name: "name", + }, + type: Some( + String, + ), + variadic: false, + default: Some( + LiteralString { + value: "foo", + }, + ), + flags: [ + Public, + Readonly, + ], + by_ref: false, + }, + ], + body: [], + flags: [ + Public, + ], + return_type: None, + by_ref: false, + }, + ], + flags: [ + Final, + ], + }, + ], + }, +] diff --git a/tests/0153/code.php b/tests/0153/code.php new file mode 100644 index 00000000..0e033676 --- /dev/null +++ b/tests/0153/code.php @@ -0,0 +1,9 @@ + Parse Error: Static property Foo\Bar\Baz:$foo cannot be readonly on line 7 column 1 diff --git a/tests/0154/tokens.txt b/tests/0154/tokens.txt new file mode 100644 index 00000000..07a5b0b2 --- /dev/null +++ b/tests/0154/tokens.txt @@ -0,0 +1,117 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Bar", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 3, + 18, + ), + }, + Token { + kind: Final, + span: ( + 5, + 1, + ), + }, + Token { + kind: Class, + span: ( + 5, + 7, + ), + }, + Token { + kind: Identifier( + "Baz", + ), + span: ( + 5, + 13, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 17, + ), + }, + Token { + kind: Public, + span: ( + 6, + 6, + ), + }, + Token { + kind: Readonly, + span: ( + 6, + 13, + ), + }, + Token { + kind: Static, + span: ( + 6, + 22, + ), + }, + Token { + kind: Identifier( + "string", + ), + span: ( + 6, + 29, + ), + }, + Token { + kind: Variable( + "foo", + ), + span: ( + 6, + 36, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 40, + ), + }, + Token { + kind: RightBrace, + span: ( + 7, + 1, + ), + }, +] diff --git a/tests/0155/ast.txt b/tests/0155/ast.txt new file mode 100644 index 00000000..4f0362a7 --- /dev/null +++ b/tests/0155/ast.txt @@ -0,0 +1,31 @@ +[ + Namespace { + name: "Foo\Bar", + body: [ + Noop, + Class { + name: Identifier { + name: "Baz", + }, + extends: None, + implements: [], + body: [ + Property { + var: "foo", + value: None, + type: Some( + String, + ), + flags: [ + Public, + Readonly, + ], + }, + ], + flags: [ + Final, + ], + }, + ], + }, +] diff --git a/tests/0155/code.php b/tests/0155/code.php new file mode 100644 index 00000000..1ac23bb3 --- /dev/null +++ b/tests/0155/code.php @@ -0,0 +1,7 @@ + Parse Error: Readonly property Foo\Bar\Baz:$foo cannot have a default value on line 7 column 1 diff --git a/tests/0156/tokens.txt b/tests/0156/tokens.txt new file mode 100644 index 00000000..94dac44f --- /dev/null +++ b/tests/0156/tokens.txt @@ -0,0 +1,126 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Bar", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 3, + 18, + ), + }, + Token { + kind: Final, + span: ( + 5, + 1, + ), + }, + Token { + kind: Class, + span: ( + 5, + 7, + ), + }, + Token { + kind: Identifier( + "Baz", + ), + span: ( + 5, + 13, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 17, + ), + }, + Token { + kind: Public, + span: ( + 6, + 6, + ), + }, + Token { + kind: Readonly, + span: ( + 6, + 13, + ), + }, + Token { + kind: Identifier( + "string", + ), + span: ( + 6, + 22, + ), + }, + Token { + kind: Variable( + "foo", + ), + span: ( + 6, + 29, + ), + }, + Token { + kind: Equals, + span: ( + 6, + 34, + ), + }, + Token { + kind: LiteralString( + "foo", + ), + span: ( + 6, + 36, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 41, + ), + }, + Token { + kind: RightBrace, + span: ( + 7, + 1, + ), + }, +]