diff --git a/src/parser/error.rs b/src/parser/error.rs index 239a940..ef38c2a 100644 --- a/src/parser/error.rs +++ b/src/parser/error.rs @@ -36,6 +36,7 @@ pub enum ParseError { MatchExpressionWithMultipleDefaultArms(Span), CannotFindTypeInCurrentScope(String, Span), ExpectedItemDefinitionAfterAttributes(Span), + NestedDisjunctiveNormalFormTypes(Span), } impl Display for ParseError { @@ -84,6 +85,7 @@ impl Display for ParseError { Self::MatchExpressionWithMultipleDefaultArms(span) => write!(f, "Parse Error: Match expressions may only contain one default arm on line {} column {}", span.0, span.1), Self::CannotFindTypeInCurrentScope(ty, span) => write!(f, "Parse Error: Cannot find type `{}` in this scope on line {} on column {}", ty, span.0, span.1), Self::ExpectedItemDefinitionAfterAttributes(span) => write!(f, "Parse Error: Expected item definition after attribute on line {} column {}", span.0, span.1), + Self::NestedDisjunctiveNormalFormTypes(span) => write!(f, "Parse Error: Nested disjunctive normal form types are not allowed on line {} column {}", span.0, span.1), } } } diff --git a/src/parser/internal/types.rs b/src/parser/internal/types.rs index bb8c515..65f771a 100644 --- a/src/parser/internal/types.rs +++ b/src/parser/internal/types.rs @@ -8,6 +8,7 @@ use crate::parser::error::ParseError; use crate::parser::error::ParseResult; use crate::parser::state::State; use crate::parser::Parser; +use crate::peek_token; impl Parser { pub(in crate::parser) fn try_block_caught_type_string( @@ -39,20 +40,42 @@ impl Parser { } pub(in crate::parser) fn get_type(&self, state: &mut State) -> ParseResult { - let ty = self.maybe_nullable(state, &|state| self.get_simple_type(state))?; - - if ty.nullable() { - return Ok(ty); + if state.current.kind == TokenKind::Question { + return self.get_nullable_type(state); } + // (A|B|..)&C.. or (A&B&..)|C.. + if state.current.kind == TokenKind::LeftParen { + state.next(); + let ty = self.get_simple_type(state)?; + return peek_token!([ + TokenKind::Pipe => { + let union = self.get_union_type(state, ty, true)?; + + self.rparen(state)?; + + self.get_intersection_type(state, union, false) + }, + TokenKind::Ampersand => { + let intersection = self.get_intersection_type(state, ty, true)?; + + self.rparen(state)?; + + self.get_union_type(state, intersection, false) + }, + ], state, ["`|`", "`&`"]); + } + + let ty = self.get_simple_type(state)?; + if state.current.kind == TokenKind::Pipe { - return self.parse_union(state, ty); + return self.get_union_type(state, ty, false); } if state.current.kind == TokenKind::Ampersand && !matches!(state.peek.kind, TokenKind::Variable(_)) { - return self.parse_intersection(state, ty); + return self.get_intersection_type(state, ty, false); } Ok(ty) @@ -63,7 +86,29 @@ impl Parser { state: &mut State, ) -> ParseResult> { if state.current.kind == TokenKind::Question { - return Ok(Some(self.get_type(state)?)); + return self.get_nullable_type(state).map(Some); + } + + // (A|B|..)&C.. or (A&B&..)|C.. + if state.current.kind == TokenKind::LeftParen { + state.next(); + let ty = self.get_simple_type(state)?; + return peek_token!([ + TokenKind::Pipe => { + let union = self.get_union_type(state, ty, true)?; + + self.rparen(state)?; + + self.get_intersection_type(state, union, false).map(Some) + }, + TokenKind::Ampersand => { + let intersection = self.get_intersection_type(state, ty, true)?; + + self.rparen(state)?; + + self.get_union_type(state, intersection, false).map(Some) + }, + ], state, ["`|`", "`&`"]); } let ty = self.get_optional_simple_type(state)?; @@ -71,13 +116,13 @@ impl Parser { match ty { Some(ty) => { if state.current.kind == TokenKind::Pipe { - return Ok(Some(self.parse_union(state, ty)?)); + return Ok(Some(self.get_union_type(state, ty, false)?)); } if state.current.kind == TokenKind::Ampersand && !matches!(state.peek.kind, TokenKind::Variable(_)) { - return Ok(Some(self.parse_intersection(state, ty)?)); + return Ok(Some(self.get_intersection_type(state, ty, false)?)); } Ok(Some(ty)) @@ -86,6 +131,21 @@ impl Parser { } } + fn get_nullable_type(&self, state: &mut State) -> ParseResult { + state.next(); + + let ty = self.get_simple_type(state)?; + + if ty.standalone() { + return Err(ParseError::StandaloneTypeUsedInCombination( + ty, + state.current.span, + )); + } + + Ok(Type::Nullable(Box::new(ty))) + } + fn get_optional_simple_type(&self, state: &mut State) -> ParseResult> { match state.current.kind.clone() { TokenKind::Array => { @@ -190,7 +250,12 @@ impl Parser { .ok_or_else(|| expected_token!(["a type"], state)) } - fn parse_union(&self, state: &mut State, other: Type) -> ParseResult { + fn get_union_type( + &self, + state: &mut State, + other: Type, + within_dnf: bool, + ) -> ParseResult { if other.standalone() { return Err(ParseError::StandaloneTypeUsedInCombination( other, @@ -203,10 +268,29 @@ impl Parser { expect_token!([TokenKind::Pipe], state, ["|"]); loop { let ty = if state.current.kind == TokenKind::LeftParen { + if within_dnf { + // don't allow nesting. + // + // examples on how we got here: + // + // v-- get_intersection_type: within_dnf = fasle + // v-- get_union_type: within_dnf = true + // v-- error + // F&(A|(D&S)) + // + // v-- get_intersection_type: within_dnf = fasle + // v-- get_union_type: within_dnf = true + // v-- error + // F&(A|B|(D&S)) + return Err(ParseError::NestedDisjunctiveNormalFormTypes( + state.current.span, + )); + } + state.next(); let other = self.get_simple_type(state)?; - let ty = self.parse_intersection(state, other)?; + let ty = self.get_intersection_type(state, other, true)?; self.rparen(state)?; @@ -235,7 +319,12 @@ impl Parser { Ok(Type::Union(types)) } - fn parse_intersection(&self, state: &mut State, other: Type) -> ParseResult { + fn get_intersection_type( + &self, + state: &mut State, + other: Type, + within_dnf: bool, + ) -> ParseResult { if other.standalone() { return Err(ParseError::StandaloneTypeUsedInCombination( other, @@ -248,10 +337,29 @@ impl Parser { expect_token!([TokenKind::Ampersand], state, ["&"]); loop { let ty = if state.current.kind == TokenKind::LeftParen { + if within_dnf { + // don't allow nesting. + // + // examples on how we got here: + // + // v-- get_union_type: within_dnf = fasle + // v-- get_intersection_type: within_dnf = true + // v-- error + // F|(A&(D|S)) + // + // v-- get_union_type: within_dnf = fasle + // v-- get_intersection_type: within_dnf = true + // v-- error + // F|(A&B&(D|S)) + return Err(ParseError::NestedDisjunctiveNormalFormTypes( + state.current.span, + )); + } + state.next(); let other = self.get_simple_type(state)?; - let ty = self.parse_union(state, other)?; + let ty = self.get_union_type(state, other, true)?; self.rparen(state)?; @@ -281,25 +389,4 @@ impl Parser { Ok(Type::Intersection(types)) } - - fn maybe_nullable( - &self, - state: &mut State, - otherwise: &(dyn Fn(&mut State) -> ParseResult), - ) -> ParseResult { - if state.current.kind == TokenKind::Question { - state.next(); - let inner = otherwise(state)?; - if inner.standalone() { - return Err(ParseError::StandaloneTypeUsedInCombination( - inner, - state.current.span, - )); - } - - Ok(Type::Nullable(Box::new(inner))) - } else { - otherwise(state) - } - } } diff --git a/tests/fixtures/0251/ast.txt b/tests/fixtures/0251/ast.txt new file mode 100644 index 0000000..da1b75d --- /dev/null +++ b/tests/fixtures/0251/ast.txt @@ -0,0 +1,154 @@ +[ + Function { + name: Identifier { + start: ( + 3, + 10, + ), + name: "bar", + end: ( + 3, + 13, + ), + }, + attributes: [], + params: [ + Param { + name: Variable { + start: ( + 4, + 13, + ), + name: "i", + end: ( + 5, + 1, + ), + }, + attributes: [], + type: Some( + Union( + [ + Identifier( + Identifier { + start: ( + 4, + 5, + ), + name: "A", + end: ( + 4, + 6, + ), + }, + ), + Intersection( + [ + Identifier( + Identifier { + start: ( + 4, + 8, + ), + name: "B", + end: ( + 4, + 9, + ), + }, + ), + Identifier( + Identifier { + start: ( + 4, + 10, + ), + name: "C", + end: ( + 4, + 11, + ), + }, + ), + ], + ), + ], + ), + ), + variadic: false, + default: None, + flags: [], + by_ref: false, + }, + ], + body: [ + Return { + value: Some( + Variable( + Variable { + start: ( + 6, + 12, + ), + name: "i", + end: ( + 6, + 14, + ), + }, + ), + ), + }, + ], + return_type: Some( + Union( + [ + Intersection( + [ + Identifier( + Identifier { + start: ( + 5, + 5, + ), + name: "B", + end: ( + 5, + 6, + ), + }, + ), + Identifier( + Identifier { + start: ( + 5, + 7, + ), + name: "C", + end: ( + 5, + 8, + ), + }, + ), + ], + ), + Identifier( + Identifier { + start: ( + 5, + 10, + ), + name: "A", + end: ( + 5, + 12, + ), + }, + ), + ], + ), + ), + by_ref: false, + }, +] diff --git a/tests/fixtures/0251/code.php b/tests/fixtures/0251/code.php new file mode 100644 index 0000000..20cbe8c --- /dev/null +++ b/tests/fixtures/0251/code.php @@ -0,0 +1,7 @@ + Parse Error: Nested disjunctive normal form types are not allowed on line 4 column 10 diff --git a/tests/fixtures/0252/tokens.txt b/tests/fixtures/0252/tokens.txt new file mode 100644 index 0000000..604c147 --- /dev/null +++ b/tests/fixtures/0252/tokens.txt @@ -0,0 +1,172 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Function, + span: ( + 3, + 1, + ), + }, + Token { + kind: Identifier( + "bar", + ), + span: ( + 3, + 10, + ), + }, + Token { + kind: LeftParen, + span: ( + 3, + 13, + ), + }, + Token { + kind: Identifier( + "F", + ), + span: ( + 4, + 5, + ), + }, + Token { + kind: Pipe, + span: ( + 4, + 6, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 7, + ), + }, + Token { + kind: Identifier( + "A", + ), + span: ( + 4, + 8, + ), + }, + Token { + kind: Ampersand, + span: ( + 4, + 9, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 10, + ), + }, + Token { + kind: Identifier( + "B", + ), + span: ( + 4, + 11, + ), + }, + Token { + kind: Pipe, + span: ( + 4, + 12, + ), + }, + Token { + kind: Identifier( + "C", + ), + span: ( + 4, + 13, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 14, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 15, + ), + }, + Token { + kind: Variable( + "i", + ), + span: ( + 4, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 5, + 1, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 3, + ), + }, + Token { + kind: Return, + span: ( + 6, + 5, + ), + }, + Token { + kind: Variable( + "i", + ), + span: ( + 6, + 12, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 14, + ), + }, + Token { + kind: RightBrace, + span: ( + 7, + 1, + ), + }, +] diff --git a/tests/fixtures/0253/code.php b/tests/fixtures/0253/code.php new file mode 100644 index 0000000..ab3ac78 --- /dev/null +++ b/tests/fixtures/0253/code.php @@ -0,0 +1,7 @@ + Parse Error: Nested disjunctive normal form types are not allowed on line 4 column 10 diff --git a/tests/fixtures/0253/tokens.txt b/tests/fixtures/0253/tokens.txt new file mode 100644 index 0000000..0c2a027 --- /dev/null +++ b/tests/fixtures/0253/tokens.txt @@ -0,0 +1,172 @@ +[ + Token { + kind: OpenTag( + Full, + ), + span: ( + 1, + 1, + ), + }, + Token { + kind: Function, + span: ( + 3, + 1, + ), + }, + Token { + kind: Identifier( + "bar", + ), + span: ( + 3, + 10, + ), + }, + Token { + kind: LeftParen, + span: ( + 3, + 13, + ), + }, + Token { + kind: Identifier( + "F", + ), + span: ( + 4, + 5, + ), + }, + Token { + kind: Ampersand, + span: ( + 4, + 6, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 7, + ), + }, + Token { + kind: Identifier( + "A", + ), + span: ( + 4, + 8, + ), + }, + Token { + kind: Pipe, + span: ( + 4, + 9, + ), + }, + Token { + kind: LeftParen, + span: ( + 4, + 10, + ), + }, + Token { + kind: Identifier( + "B", + ), + span: ( + 4, + 11, + ), + }, + Token { + kind: Ampersand, + span: ( + 4, + 12, + ), + }, + Token { + kind: Identifier( + "C", + ), + span: ( + 4, + 13, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 14, + ), + }, + Token { + kind: RightParen, + span: ( + 4, + 15, + ), + }, + Token { + kind: Variable( + "i", + ), + span: ( + 4, + 17, + ), + }, + Token { + kind: RightParen, + span: ( + 5, + 1, + ), + }, + Token { + kind: LeftBrace, + span: ( + 5, + 3, + ), + }, + Token { + kind: Return, + span: ( + 6, + 5, + ), + }, + Token { + kind: Variable( + "i", + ), + span: ( + 6, + 12, + ), + }, + Token { + kind: SemiColon, + span: ( + 6, + 14, + ), + }, + Token { + kind: RightBrace, + span: ( + 7, + 1, + ), + }, +] diff --git a/tests/third_party_tests.rs b/tests/third_party_tests.rs index 4dd2539..93c63b3 100644 --- a/tests/third_party_tests.rs +++ b/tests/third_party_tests.rs @@ -41,15 +41,13 @@ fn third_party_3_symfony_framework() { // FIXME: Remove this one once I've found the energy to sort out heredocs / nowdocs. "src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php", "src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php", - // FIXME: Remove this once we can support (A&B)|C DNF types. - "src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/compositetype_classes.php", // FIXME: Remove these once we can support arbitrary opening and closing tags. "src/Symfony/Component/ErrorHandler/Resources/views/exception.html.php", "src/Symfony/Component/ErrorHandler/Resources/views/exception_full.html.php", "src/Symfony/Component/ErrorHandler/Resources/views/logs.html.php", "src/Symfony/Component/ErrorHandler/Resources/views/trace.html.php", "src/Symfony/Component/ErrorHandler/Resources/views/traces.html.php", - "src/Symfony/Component/ErrorHandler/Resources/views/traces_text.html.php" + "src/Symfony/Component/ErrorHandler/Resources/views/traces_text.html.php", ], ); }