diff --git a/src/parser/error.rs b/src/parser/error.rs index 870ca93..00e9edb 100644 --- a/src/parser/error.rs +++ b/src/parser/error.rs @@ -30,6 +30,8 @@ pub enum ParseError { UnpredictableState(Span), StaticPropertyUsingReadonlyModifier(String, String, Span), ReadonlyPropertyHasDefaultValue(String, String, Span), + MixingBracedAndUnBracedNamespaceDeclarations(Span), + NestedNamespaceDeclarations(Span), } impl Display for ParseError { @@ -72,6 +74,8 @@ impl Display for ParseError { 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), + Self::MixingBracedAndUnBracedNamespaceDeclarations(span) => write!(f, "Parse Error: Cannot mix braced namespace declarations with unbraced namespace declarations on line {} column {}", span.0, span.1), + Self::NestedNamespaceDeclarations(span) => write!(f, "Parse Error: Namespace declarations cannot be mixed on line {} column {}", span.0, span.1), } } } diff --git a/src/parser/internal/ident.rs b/src/parser/internal/ident.rs index c5c8faf..9c0a003 100644 --- a/src/parser/internal/ident.rs +++ b/src/parser/internal/ident.rs @@ -16,10 +16,21 @@ impl Parser { /// Expect an unqualified or qualified identifier such as Foo, Bar or Foo\Bar. pub(in crate::parser) fn name(&self, state: &mut State) -> ParseResult { - Ok(expect_token!([ - TokenKind::Identifier(identifier) => identifier, - TokenKind::QualifiedIdentifier(qualified) => qualified, - ], state, "an identifier")) + expect_token!([ + TokenKind::Identifier(name) | TokenKind::QualifiedIdentifier(name) => Ok(name), + ], state, "an identifier") + } + + /// Expect an optional unqualified or qualified identifier such as Foo, Bar or Foo\Bar. + pub(in crate::parser) fn optional_name(&self, state: &mut State) -> Option { + match state.current.kind.clone() { + TokenKind::Identifier(name) | TokenKind::QualifiedIdentifier(name) => { + state.next(); + + Some(name) + } + _ => None, + } } /// Expect an unqualified, qualified or fully qualified identifier such as Foo, Foo\Bar or \Foo\Bar. diff --git a/src/parser/internal/mod.rs b/src/parser/internal/mod.rs index 27d965d..88a216b 100644 --- a/src/parser/internal/mod.rs +++ b/src/parser/internal/mod.rs @@ -4,6 +4,7 @@ pub mod classish_statement; pub mod flags; pub mod functions; pub mod ident; +pub mod namespace; pub mod params; pub mod precedence; pub mod punc; diff --git a/src/parser/internal/namespace.rs b/src/parser/internal/namespace.rs new file mode 100644 index 0000000..ffdc7b5 --- /dev/null +++ b/src/parser/internal/namespace.rs @@ -0,0 +1,86 @@ +use crate::lexer::token::TokenKind; +use crate::parser::ast::Block; +use crate::parser::ast::Statement; +use crate::parser::error::ParseError; +use crate::parser::error::ParseResult; +use crate::parser::state::NamespaceType; +use crate::parser::state::Scope; +use crate::parser::state::State; +use crate::parser::Parser; +use crate::prelude::ByteString; +use crate::scoped; + +impl Parser { + pub(in crate::parser) fn namespace(&self, state: &mut State) -> ParseResult { + state.next(); + + let name = self.optional_name(state); + + if let Some(name) = &name { + if state.current.kind != TokenKind::LeftBrace { + match state.namespace_type() { + Some(NamespaceType::Braced) => { + return Err(ParseError::MixingBracedAndUnBracedNamespaceDeclarations( + state.current.span, + )); + } + Some(NamespaceType::Unbraced) => { + // exit the current namespace scope. + // we don't need to check if the current scope is a namespace + // because we know it is, it is not possible for it to be anything else. + // as using `namespace` anywhere aside from a top-level stmt would result + // in a parse error. + state.exit(); + } + _ => {} + } + + return self.unbraced_namespace(state, name.clone()); + } + } + + match state.namespace_type() { + Some(NamespaceType::Unbraced) => Err( + ParseError::MixingBracedAndUnBracedNamespaceDeclarations(state.current.span), + ), + Some(NamespaceType::Braced) if state.namespace().is_some() => { + Err(ParseError::NestedNamespaceDeclarations(state.current.span)) + } + _ => self.braced_namespace(state, name), + } + } + + fn unbraced_namespace(&self, state: &mut State, name: ByteString) -> ParseResult { + let body = scoped!(state, Scope::Namespace(name.clone()), { + let mut body = Block::new(); + while !state.is_eof() { + body.push(self.top_level_statement(state)?); + } + + Ok(body) + })?; + + Ok(Statement::Namespace { name, body }) + } + + fn braced_namespace( + &self, + state: &mut State, + name: Option, + ) -> ParseResult { + self.lbrace(state)?; + + let body = scoped!(state, Scope::BracedNamespace(name.clone()), { + let mut body = Block::new(); + while state.current.kind != TokenKind::RightBrace && !state.is_eof() { + body.push(self.top_level_statement(state)?); + } + + Ok(body) + })?; + + self.rbrace(state)?; + + Ok(Statement::BracedNamespace { name, body }) + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 63adba7..ae0952b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11,9 +11,7 @@ use crate::parser::error::ParseError; use crate::parser::error::ParseResult; use crate::parser::internal::ident::is_reserved_ident; use crate::parser::internal::precedence::{Associativity, Precedence}; -use crate::parser::state::Scope; use crate::parser::state::State; -use crate::scoped; pub mod ast; pub mod error; @@ -62,49 +60,7 @@ impl Parser { state.skip_comments(); let statement = match &state.current.kind { - TokenKind::Namespace => { - state.next(); - - if state.current.kind != TokenKind::LeftBrace { - let name = self.name(state)?; - - if state.current.kind == TokenKind::LeftBrace { - self.lbrace(state)?; - - let body = scoped!(state, Scope::BracedNamespace(Some(name.clone())), { - self.block(state, &TokenKind::RightBrace) - })?; - - self.rbrace(state)?; - - Statement::BracedNamespace { - name: Some(name), - body, - } - } else { - let body = scoped!(state, Scope::Namespace(name.clone()), { - let mut body = Block::new(); - while !state.is_eof() { - body.push(self.top_level_statement(state)?); - } - - Ok(body) - })?; - - Statement::Namespace { name, body } - } - } else { - self.lbrace(state)?; - - let body = scoped!(state, Scope::BracedNamespace(None), { - self.block(state, &TokenKind::RightBrace) - })?; - - self.rbrace(state)?; - - Statement::BracedNamespace { name: None, body } - } - } + TokenKind::Namespace => self.namespace(state)?, TokenKind::Use => { state.next(); @@ -521,11 +477,7 @@ impl Parser { }; let mut cases = Vec::new(); - loop { - if state.current.kind == end_token { - break; - } - + while state.current.kind != end_token { match state.current.kind { TokenKind::Case => { state.next(); diff --git a/src/parser/state.rs b/src/parser/state.rs index d9aace4..9b7a1ed 100644 --- a/src/parser/state.rs +++ b/src/parser/state.rs @@ -9,6 +9,12 @@ use crate::parser::ast::MethodFlag; use crate::parser::error::ParseError; use crate::parser::error::ParseResult; +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum NamespaceType { + Braced, + Unbraced, +} + #[derive(Debug, PartialEq, Eq, Clone)] pub enum Scope { Namespace(ByteString), @@ -33,6 +39,7 @@ pub struct State { pub peek: Token, pub iter: IntoIter, pub comments: Vec, + pub namespace_type: Option, } impl State { @@ -45,30 +52,40 @@ impl State { peek: iter.next().unwrap_or_default(), iter, comments: vec![], + namespace_type: None, } } - pub fn named(&self, name: &ByteString) -> String { - let mut namespace = None; + /// Return the namespace type used in the current state + /// + /// The namespace type is retrieve from the last entered + /// namespace scope. + /// + /// Note: even when a namespace scope is exited, the namespace type + /// is retained, until the next namespace scope is entered. + pub fn namespace_type(&self) -> Option { + self.namespace_type.clone() + } + + pub fn namespace(&self) -> Option<&Scope> { for scope in &self.stack { match scope { - Scope::Namespace(n) => { - namespace = Some(n.to_string()); - - break; - } - Scope::BracedNamespace(n) => { - namespace = n.as_ref().map(|s| s.to_string()); - - break; + Scope::Namespace(_) | Scope::BracedNamespace(_) => { + return Some(scope); } _ => {} } } - match namespace { - Some(v) => format!("{}\\{}", v, name), - None => name.to_string(), + None + } + + pub fn named(&self, name: &ByteString) -> String { + match self.namespace() { + Some(Scope::Namespace(n)) | Some(Scope::BracedNamespace(Some(n))) => { + format!("{}\\{}", n, name) + } + _ => name.to_string(), } } @@ -84,8 +101,18 @@ impl State { .ok_or(ParseError::UnpredictableState(self.current.span)) } - pub fn enter(&mut self, state: Scope) { - self.stack.push_back(state); + pub fn enter(&mut self, scope: Scope) { + match &scope { + Scope::Namespace(_) => { + self.namespace_type = Some(NamespaceType::Unbraced); + } + Scope::BracedNamespace(_) => { + self.namespace_type = Some(NamespaceType::Braced); + } + _ => {} + } + + self.stack.push_back(scope); } pub fn exit(&mut self) { diff --git a/tests/0157/ast.txt b/tests/0157/ast.txt new file mode 100644 index 0000000..03d23e7 --- /dev/null +++ b/tests/0157/ast.txt @@ -0,0 +1,18 @@ +[ + BracedNamespace { + name: Some( + "Foo\Bar", + ), + body: [ + Function { + name: Identifier { + name: "foo", + }, + params: [], + body: [], + return_type: None, + by_ref: false, + }, + ], + }, +] diff --git a/tests/0157/code.php b/tests/0157/code.php new file mode 100644 index 0000000..9c10766 --- /dev/null +++ b/tests/0157/code.php @@ -0,0 +1,5 @@ + Parse Error: Cannot mix braced namespace declarations with unbraced namespace declarations on line 7 column 19 diff --git a/tests/0159/tokens.txt b/tests/0159/tokens.txt new file mode 100644 index 0000000..89a7dc9 --- /dev/null +++ b/tests/0159/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: Function, + span: ( + 5, + 1, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 5, + 10, + ), + }, + Token { + kind: LeftParen, + span: ( + 5, + 13, + ), + }, + Token { + kind: RightParen, + span: ( + 5, + 14, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 16, + ), + }, + Token { + kind: RightBrace, + span: ( + 5, + 17, + ), + }, + Token { + kind: Namespace, + span: ( + 7, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Baz", + ), + span: ( + 7, + 11, + ), + }, + Token { + kind: LeftBrace, + span: ( + 7, + 19, + ), + }, + Token { + kind: Function, + span: ( + 8, + 5, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 8, + 14, + ), + }, + Token { + kind: LeftParen, + span: ( + 8, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 8, + 18, + ), + }, + Token { + kind: LeftBrace, + span: ( + 8, + 20, + ), + }, + Token { + kind: RightBrace, + span: ( + 8, + 21, + ), + }, + Token { + kind: RightBrace, + span: ( + 9, + 1, + ), + }, +] diff --git a/tests/0160/code.php b/tests/0160/code.php new file mode 100644 index 0000000..5687cf6 --- /dev/null +++ b/tests/0160/code.php @@ -0,0 +1,9 @@ + Parse Error: Cannot mix braced namespace declarations with unbraced namespace declarations on line 6 column 22 diff --git a/tests/0160/tokens.txt b/tests/0160/tokens.txt new file mode 100644 index 0000000..c4a787c --- /dev/null +++ b/tests/0160/tokens.txt @@ -0,0 +1,152 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Baz", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: LeftBrace, + span: ( + 3, + 19, + ), + }, + Token { + kind: Function, + span: ( + 4, + 5, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 4, + 14, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 18, + ), + }, + Token { + kind: LeftBrace, + span: ( + 4, + 20, + ), + }, + Token { + kind: RightBrace, + span: ( + 4, + 21, + ), + }, + Token { + kind: Namespace, + span: ( + 6, + 5, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Bar", + ), + span: ( + 6, + 15, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 22, + ), + }, + Token { + kind: Function, + span: ( + 8, + 5, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 8, + 14, + ), + }, + Token { + kind: LeftParen, + span: ( + 8, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 8, + 18, + ), + }, + Token { + kind: LeftBrace, + span: ( + 8, + 20, + ), + }, + Token { + kind: RightBrace, + span: ( + 8, + 21, + ), + }, + Token { + kind: RightBrace, + span: ( + 9, + 1, + ), + }, +] diff --git a/tests/0161/code.php b/tests/0161/code.php new file mode 100644 index 0000000..81e51d3 --- /dev/null +++ b/tests/0161/code.php @@ -0,0 +1,9 @@ + Parse Error: Cannot mix braced namespace declarations with unbraced namespace declarations on line 7 column 18 diff --git a/tests/0161/tokens.txt b/tests/0161/tokens.txt new file mode 100644 index 0000000..6b272e2 --- /dev/null +++ b/tests/0161/tokens.txt @@ -0,0 +1,152 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Baz", + ), + span: ( + 3, + 11, + ), + }, + Token { + kind: LeftBrace, + span: ( + 3, + 19, + ), + }, + Token { + kind: Function, + span: ( + 4, + 5, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 4, + 14, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 18, + ), + }, + Token { + kind: LeftBrace, + span: ( + 4, + 20, + ), + }, + Token { + kind: RightBrace, + span: ( + 4, + 21, + ), + }, + Token { + kind: RightBrace, + span: ( + 5, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 7, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Bar", + ), + span: ( + 7, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 7, + 18, + ), + }, + Token { + kind: Function, + span: ( + 9, + 1, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 9, + 10, + ), + }, + Token { + kind: LeftParen, + span: ( + 9, + 13, + ), + }, + Token { + kind: RightParen, + span: ( + 9, + 14, + ), + }, + Token { + kind: LeftBrace, + span: ( + 9, + 16, + ), + }, + Token { + kind: RightBrace, + span: ( + 9, + 17, + ), + }, +] diff --git a/tests/0162/code.php b/tests/0162/code.php new file mode 100644 index 0000000..0fe9720 --- /dev/null +++ b/tests/0162/code.php @@ -0,0 +1,9 @@ + Parse Error: Cannot mix braced namespace declarations with unbraced namespace declarations on line 7 column 18 diff --git a/tests/0162/tokens.txt b/tests/0162/tokens.txt new file mode 100644 index 0000000..d2a89a4 --- /dev/null +++ b/tests/0162/tokens.txt @@ -0,0 +1,143 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: LeftBrace, + span: ( + 3, + 11, + ), + }, + Token { + kind: Function, + span: ( + 4, + 5, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 4, + 14, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 18, + ), + }, + Token { + kind: LeftBrace, + span: ( + 4, + 20, + ), + }, + Token { + kind: RightBrace, + span: ( + 4, + 21, + ), + }, + Token { + kind: RightBrace, + span: ( + 5, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 7, + 1, + ), + }, + Token { + kind: QualifiedIdentifier( + "Foo\Bar", + ), + span: ( + 7, + 11, + ), + }, + Token { + kind: SemiColon, + span: ( + 7, + 18, + ), + }, + Token { + kind: Function, + span: ( + 9, + 1, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 9, + 10, + ), + }, + Token { + kind: LeftParen, + span: ( + 9, + 13, + ), + }, + Token { + kind: RightParen, + span: ( + 9, + 14, + ), + }, + Token { + kind: LeftBrace, + span: ( + 9, + 16, + ), + }, + Token { + kind: RightBrace, + span: ( + 9, + 17, + ), + }, +] diff --git a/tests/0163/code.php b/tests/0163/code.php new file mode 100644 index 0000000..ff0eca2 --- /dev/null +++ b/tests/0163/code.php @@ -0,0 +1,5 @@ + Parse Error: unexpected token `;`, expecting `{` on line 3 column 10 diff --git a/tests/0163/tokens.txt b/tests/0163/tokens.txt new file mode 100644 index 0000000..46fc5c5 --- /dev/null +++ b/tests/0163/tokens.txt @@ -0,0 +1,69 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Namespace, + span: ( + 3, + 1, + ), + }, + Token { + kind: SemiColon, + span: ( + 3, + 10, + ), + }, + Token { + kind: Function, + span: ( + 5, + 1, + ), + }, + Token { + kind: Identifier( + "foo", + ), + span: ( + 5, + 10, + ), + }, + Token { + kind: LeftParen, + span: ( + 5, + 13, + ), + }, + Token { + kind: RightParen, + span: ( + 5, + 14, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 16, + ), + }, + Token { + kind: RightBrace, + span: ( + 5, + 17, + ), + }, +] diff --git a/tests/0164/ast.txt b/tests/0164/ast.txt new file mode 100644 index 0000000..a169938 --- /dev/null +++ b/tests/0164/ast.txt @@ -0,0 +1,32 @@ +[ + BracedNamespace { + name: None, + body: [ + Function { + name: Identifier { + name: "foo", + }, + params: [], + body: [], + return_type: None, + by_ref: false, + }, + ], + }, + BracedNamespace { + name: Some( + "a", + ), + body: [ + Function { + name: Identifier { + name: "foo", + }, + params: [], + body: [], + return_type: None, + by_ref: false, + }, + ], + }, +] diff --git a/tests/0164/code.php b/tests/0164/code.php new file mode 100644 index 0000000..f137830 --- /dev/null +++ b/tests/0164/code.php @@ -0,0 +1,9 @@ +