From bd2023c3c4a36c8c852c0f919a9bc56189c95e77 Mon Sep 17 00:00:00 2001 From: Saif Eddin Gmati <29315886+azjezz@users.noreply.github.com> Date: Thu, 8 Dec 2022 21:34:16 +0100 Subject: [PATCH] chore: remove fallback expression parser (#181) Signed-off-by: azjezz --- src/lexer/mod.rs | 7 + src/parser/ast/mod.rs | 20 +- src/parser/expressions.rs | 448 ++++++++++++++++++----------- src/parser/internal/loops.rs | 28 +- src/parser/internal/precedences.rs | 10 - src/parser/internal/utils.rs | 2 +- tests/fixtures/0006/ast.txt | 16 ++ tests/fixtures/0010/ast.txt | 4 + tests/fixtures/0011/ast.txt | 4 + tests/fixtures/0017/ast.txt | 16 ++ tests/fixtures/0072/ast.txt | 4 + tests/fixtures/0103/ast.txt | 4 + tests/fixtures/0237/ast.txt | 10 +- tests/fixtures/0238/ast.txt | 10 +- tests/fixtures/0269/ast.txt | 4 + tests/fixtures/0276 copy/ast.txt | 74 +++++ tests/fixtures/0276 copy/code.php | 10 + tests/fixtures/0276/code.php | 2 +- 18 files changed, 473 insertions(+), 200 deletions(-) create mode 100644 tests/fixtures/0276 copy/ast.txt create mode 100644 tests/fixtures/0276 copy/code.php diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 2d0c03d..cda3ec3 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -1514,6 +1514,13 @@ fn identifier_to_keyword(ident: &[u8]) -> Option { b"var" => TokenKind::Var, b"yield" => TokenKind::Yield, b"__DIR__" => TokenKind::DirConstant, + b"__FILE__" => TokenKind::FileConstant, + b"__LINE__" => TokenKind::LineConstant, + b"__FUNCTION__" => TokenKind::FunctionConstant, + b"__CLASS__" => TokenKind::ClassConstant, + b"__METHOD__" => TokenKind::MethodConstant, + b"__TRAIT__" => TokenKind::TraitConstant, + b"__NAMESPACE__" => TokenKind::NamespaceConstant, b"while" => TokenKind::While, b"insteadof" => TokenKind::Insteadof, b"list" => TokenKind::List, diff --git a/src/parser/ast/mod.rs b/src/parser/ast/mod.rs index c939d53..1283ac0 100644 --- a/src/parser/ast/mod.rs +++ b/src/parser/ast/mod.rs @@ -391,6 +391,7 @@ pub enum Expression { Empty, VariadicPlaceholder, ErrorSuppress { + span: Span, expr: Box, }, Increment { @@ -415,6 +416,7 @@ pub enum Expression { rhs: Box, }, Include { + span: Span, kind: IncludeKind, path: Box, }, @@ -496,9 +498,11 @@ pub enum Expression { }, Null, BooleanNot { + span: Span, value: Box, }, MagicConst { + span: Span, constant: MagicConst, }, Ternary { @@ -529,24 +533,31 @@ pub enum Expression { value: Box, }, Negate { + span: Span, value: Box, }, UnaryPlus { + span: Span, value: Box, }, BitwiseNot { + span: Span, value: Box, }, PreDecrement { + span: Span, value: Box, }, PreIncrement { + span: Span, value: Box, }, Print { + span: Span, value: Box, }, Cast { + span: Span, kind: CastKind, value: Box, }, @@ -578,7 +589,14 @@ pub struct MatchArm { #[derive(Debug, Eq, PartialEq, Clone)] pub enum MagicConst { - Dir, + Directory, + File, + Line, + Class, + Function, + Method, + Namespace, + Trait, } #[derive(Debug, PartialEq, Clone)] diff --git a/src/parser/expressions.rs b/src/parser/expressions.rs index c3f12c8..d0fe83c 100644 --- a/src/parser/expressions.rs +++ b/src/parser/expressions.rs @@ -188,7 +188,7 @@ fn create(state: &mut State) -> ParseResult { } macro_rules! expressions { - ($(#[before($else:ident), current($( $current:pat_param )|+) $(, peek($( $peek:pat_param )|+))?] $expr:ident($out:expr))+) => { + ($(#[before($else:ident), current($(|)? $( $current:pat_param )|+) $(, peek($(|)? $( $peek:pat_param )|+))?] $expr:ident($out:expr))+) => { $( #[inline(never)] fn $expr(state: &mut State) -> ParseResult { @@ -481,107 +481,286 @@ expressions! { arrays::legacy_array_expression(state) }) - #[before(fallback), current(TokenKind::LeftBracket)] + #[before(new), current(TokenKind::LeftBracket)] left_bracket(|state: &mut State| { arrays::array_expression(state) }) + + #[before(directory_magic_constant), current(TokenKind::New)] + new(|state: &mut State| { + state.next(); + let target = match state.current.kind { + TokenKind::Self_ => { + if !state.has_class_scope { + return Err(ParseError::CannotFindTypeInCurrentScope( + state.current.kind.to_string(), + state.current.span, + )); + } + + state.next(); + + Expression::Self_ + } + TokenKind::Static => { + if !state.has_class_scope { + return Err(ParseError::CannotFindTypeInCurrentScope( + state.current.kind.to_string(), + state.current.span, + )); + } + + state.next(); + + Expression::Static + } + TokenKind::Parent => { + if !state.has_class_scope { + return Err(ParseError::CannotFindTypeInCurrentScope( + state.current.kind.to_string(), + state.current.span, + )); + } + + state.next(); + + Expression::Parent + } + _ => clone_or_new_precedence(state)?, + }; + + let mut args = vec![]; + if state.current.kind == TokenKind::LeftParen { + args = parameters::args_list(state)?; + } + + Ok(Expression::New{target:Box::new(target),args,}) + }) + + #[before(file_magic_constant), current(TokenKind::DirConstant)] + directory_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Directory + }) + }) + + #[before(line_magic_constant), current(TokenKind::FileConstant)] + file_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::File + }) + }) + + #[before(function_magic_constant), current(TokenKind::LineConstant)] + line_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Line + }) + }) + + #[before(class_magic_constant), current(TokenKind::FunctionConstant)] + function_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Function, + }) + }) + + #[before(method_magic_constant), current(TokenKind::ClassConstant)] + class_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Class, + }) + }) + + #[before(namespace_magic_constant), current(TokenKind::MethodConstant)] + method_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Method, + }) + }) + + #[before(trait_magic_constant), current(TokenKind::NamespaceConstant)] + namespace_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Namespace, + }) + }) + + #[before(include), current(TokenKind::TraitConstant)] + trait_magic_constant(|state: &mut State| { + let span = state.current.span; + state.next(); + + Ok(Expression::MagicConst { + span, + constant: MagicConst::Trait + }) + }) + + #[before(cast_prefix), current(TokenKind::Include | TokenKind::IncludeOnce | TokenKind::Require | TokenKind::RequireOnce)] + include(|state: &mut State| { + let kind: IncludeKind = (&state.current.kind).into(); + let span = state.current.span; + + state.next(); + + let path = lowest_precedence(state)?; + + Ok(Expression::Include { + span, + kind, + path:Box::new(path) + }) + }) + + #[before(numeric_prefix), current( + | TokenKind::StringCast | TokenKind::BinaryCast | TokenKind::ObjectCast + | TokenKind::BoolCast | TokenKind::BooleanCast | TokenKind::IntCast + | TokenKind::IntegerCast | TokenKind::FloatCast | TokenKind::DoubleCast + | TokenKind::RealCast | TokenKind::UnsetCast | TokenKind::ArrayCast + )] + cast_prefix(|state: &mut State| { + let span = state.current.span; + let kind = state.current.kind.clone().into(); + + state.next(); + + let rhs = for_precedence(state, Precedence::Prefix)?; + + Ok(Expression::Cast { + span, + kind, + value: Box::new(rhs), + }) + }) + + #[before(bang_prefix), current(TokenKind::Decrement | TokenKind::Increment | TokenKind::Minus | TokenKind::Plus)] + numeric_prefix(|state: &mut State| { + let span = state.current.span; + let op = state.current.kind.clone(); + + state.next(); + + let rhs = for_precedence(state, Precedence::Prefix)?; + + let expr = match op { + TokenKind::Minus => Expression::Negate { + span, + value: Box::new(rhs), + }, + TokenKind::Plus => Expression::UnaryPlus { + span, + value: Box::new(rhs), + }, + TokenKind::Decrement => Expression::PreDecrement { + span, + value: Box::new(rhs), + }, + TokenKind::Increment => Expression::PreIncrement { + span, + value: Box::new(rhs), + }, + _ => unreachable!(), + }; + + Ok(expr) + }) + + #[before(at_prefix), current(TokenKind::Bang)] + bang_prefix(|state: &mut State| { + let span = state.current.span; + + state.next(); + + let rhs = for_precedence(state, Precedence::Bang)?; + + Ok(Expression::BooleanNot { + span, + value: Box::new(rhs) + }) + }) + + #[before(print_prefix), current(TokenKind::At)] + at_prefix(|state: &mut State| { + let span = state.current.span; + + state.next(); + + let rhs = for_precedence(state, Precedence::Prefix)?; + + Ok(Expression::ErrorSuppress { + span, + expr: Box::new(rhs) + }) + }) + + #[before(bitwise_prefix), current(TokenKind::Print)] + print_prefix(|state: &mut State| { + let span = state.current.span; + + state.next(); + + let rhs = for_precedence(state, Precedence::Prefix)?; + + Ok(Expression::Print { + span, + value: Box::new(rhs) + }) + }) + + #[before(dynamic_variable), current(TokenKind::BitwiseNot)] + bitwise_prefix(|state: &mut State| { + let span = state.current.span; + + state.next(); + + let rhs = for_precedence(state, Precedence::Prefix)?; + + Ok(Expression::BitwiseNot { + span, + value: Box::new(rhs) + }) + }) + + #[before(unexpected_token), current(TokenKind::Dollar)] + dynamic_variable(|state: &mut State| { + variables::dynamic_variable(state) + }) } -fn fallback(state: &mut State) -> ParseResult { - let expr = match &state.current.kind { - TokenKind::New => { - utils::skip(state, TokenKind::New)?; - - let target = match state.current.kind { - TokenKind::Self_ => { - if !state.has_class_scope { - return Err(ParseError::CannotFindTypeInCurrentScope( - state.current.kind.to_string(), - state.current.span, - )); - } - - state.next(); - - Expression::Self_ - } - TokenKind::Static => { - if !state.has_class_scope { - return Err(ParseError::CannotFindTypeInCurrentScope( - state.current.kind.to_string(), - state.current.span, - )); - } - - state.next(); - - Expression::Static - } - TokenKind::Parent => { - if !state.has_class_scope { - return Err(ParseError::CannotFindTypeInCurrentScope( - state.current.kind.to_string(), - state.current.span, - )); - } - - state.next(); - - Expression::Parent - } - _ => clone_or_new_precedence(state)?, - }; - - let mut args = vec![]; - if state.current.kind == TokenKind::LeftParen { - args = parameters::args_list(state)?; - } - - Expression::New { - target: Box::new(target), - args, - } - } - TokenKind::DirConstant => { - state.next(); - Expression::MagicConst { - constant: MagicConst::Dir, - } - } - TokenKind::Include - | TokenKind::IncludeOnce - | TokenKind::Require - | TokenKind::RequireOnce => { - let kind: IncludeKind = (&state.current.kind).into(); - state.next(); - - let path = lowest_precedence(state)?; - - Expression::Include { - kind, - path: Box::new(path), - } - } - _ if is_prefix(&state.current.kind) => { - let op = state.current.kind.clone(); - - state.next(); - - let rpred = Precedence::prefix(&op); - let rhs = for_precedence(state, rpred)?; - - prefix(&op, rhs) - } - TokenKind::Dollar => variables::dynamic_variable(state)?, - _ => { - return Err(ParseError::UnexpectedToken( - state.current.kind.to_string(), - state.current.span, - )) - } - }; - - Ok(expr) +fn unexpected_token(state: &mut State) -> ParseResult { + Err(ParseError::UnexpectedToken( + state.current.kind.to_string(), + state.current.span, + )) } fn postfix(state: &mut State, lhs: Expression, op: &TokenKind) -> Result { @@ -949,9 +1128,11 @@ fn interpolated_string_part(state: &mut State) -> ParseResult e } TokenKind::Minus => { + let span = state.current.span; state.next(); if let TokenKind::LiteralInteger(i) = &state.current.kind { let e = Expression::Negate { + span, value: Box::new(Expression::LiteralInteger { i: i.clone() }), }; state.next(); @@ -1014,79 +1195,6 @@ fn interpolated_string_part(state: &mut State) -> ParseResult }) } -#[inline(always)] -fn is_prefix(op: &TokenKind) -> bool { - matches!( - op, - TokenKind::Bang - | TokenKind::Print - | TokenKind::BitwiseNot - | TokenKind::Decrement - | TokenKind::Increment - | TokenKind::Minus - | TokenKind::Plus - | TokenKind::StringCast - | TokenKind::BinaryCast - | TokenKind::ObjectCast - | TokenKind::BoolCast - | TokenKind::BooleanCast - | TokenKind::IntCast - | TokenKind::IntegerCast - | TokenKind::FloatCast - | TokenKind::DoubleCast - | TokenKind::RealCast - | TokenKind::UnsetCast - | TokenKind::ArrayCast - | TokenKind::At - ) -} - -#[inline(always)] -fn prefix(op: &TokenKind, rhs: Expression) -> Expression { - match op { - TokenKind::Print => Expression::Print { - value: Box::new(rhs), - }, - TokenKind::Bang => Expression::BooleanNot { - value: Box::new(rhs), - }, - TokenKind::Minus => Expression::Negate { - value: Box::new(rhs), - }, - TokenKind::Plus => Expression::UnaryPlus { - value: Box::new(rhs), - }, - TokenKind::BitwiseNot => Expression::BitwiseNot { - value: Box::new(rhs), - }, - TokenKind::Decrement => Expression::PreDecrement { - value: Box::new(rhs), - }, - TokenKind::Increment => Expression::PreIncrement { - value: Box::new(rhs), - }, - TokenKind::StringCast - | TokenKind::BinaryCast - | TokenKind::ObjectCast - | TokenKind::BoolCast - | TokenKind::BooleanCast - | TokenKind::IntCast - | TokenKind::IntegerCast - | TokenKind::FloatCast - | TokenKind::DoubleCast - | TokenKind::RealCast - | TokenKind::UnsetCast - | TokenKind::ArrayCast => Expression::Cast { - kind: op.into(), - value: Box::new(rhs), - }, - TokenKind::At => Expression::ErrorSuppress { - expr: Box::new(rhs), - }, - _ => unreachable!(), - } -} - fn is_infix(t: &TokenKind) -> bool { matches!( t, diff --git a/src/parser/internal/loops.rs b/src/parser/internal/loops.rs index 866096f..45a0755 100644 --- a/src/parser/internal/loops.rs +++ b/src/parser/internal/loops.rs @@ -1,11 +1,11 @@ use crate::lexer::token::TokenKind; +use crate::parser; use crate::parser::ast::Statement; use crate::parser::error::ParseResult; use crate::parser::expressions; use crate::parser::internal::blocks; use crate::parser::internal::utils; use crate::parser::state::State; -use crate::parser; pub fn foreach_loop(state: &mut State) -> ParseResult { utils::skip(state, TokenKind::Foreach)?; @@ -175,21 +175,19 @@ pub fn while_loop(state: &mut State) -> ParseResult { let body = if state.current.kind == TokenKind::SemiColon { utils::skip_semicolon(state)?; vec![] + } else if state.current.kind == TokenKind::Colon { + utils::skip_colon(state)?; + let then = blocks::body(state, &TokenKind::EndWhile)?; + utils::skip(state, TokenKind::EndWhile)?; + utils::skip_semicolon(state)?; + then + } else if state.current.kind == TokenKind::LeftBrace { + utils::skip_left_brace(state)?; + let then = blocks::body(state, &TokenKind::RightBrace)?; + utils::skip_right_brace(state)?; + then } else { - if state.current.kind == TokenKind::Colon { - utils::skip_colon(state)?; - let then = blocks::body(state, &TokenKind::EndWhile)?; - utils::skip(state, TokenKind::EndWhile)?; - utils::skip_semicolon(state)?; - then - } else if state.current.kind == TokenKind::LeftBrace { - utils::skip_left_brace(state)?; - let then = blocks::body(state, &TokenKind::RightBrace)?; - utils::skip_right_brace(state)?; - then - } else { - vec![parser::statement(state)?] - } + vec![parser::statement(state)?] }; Ok(Statement::While { condition, body }) diff --git a/src/parser/internal/precedences.rs b/src/parser/internal/precedences.rs index 6605f0d..04bb508 100644 --- a/src/parser/internal/precedences.rs +++ b/src/parser/internal/precedences.rs @@ -41,16 +41,6 @@ pub enum Precedence { } impl Precedence { - pub fn prefix(kind: &TokenKind) -> Self { - use TokenKind::*; - - match kind { - Bang => Self::Bang, - Clone | New => Self::CloneOrNew, - _ => Self::Prefix, - } - } - pub fn infix(kind: &TokenKind) -> Self { use TokenKind::*; diff --git a/src/parser/internal/utils.rs b/src/parser/internal/utils.rs index 8d3a0d8..0752b00 100644 --- a/src/parser/internal/utils.rs +++ b/src/parser/internal/utils.rs @@ -22,7 +22,7 @@ pub fn skip_semicolon(state: &mut State) -> ParseResult { return Err(ParseError::ExpectedToken( vec!["`;`".to_string()], found, - state.current.span, + end, )); } else { state.next(); diff --git a/tests/fixtures/0006/ast.txt b/tests/fixtures/0006/ast.txt index 8bc5bda..45d30fe 100644 --- a/tests/fixtures/0006/ast.txt +++ b/tests/fixtures/0006/ast.txt @@ -1,6 +1,10 @@ [ Expression { expr: Include { + span: ( + 3, + 1, + ), kind: Include, path: LiteralString { value: "foo.php", @@ -9,6 +13,10 @@ }, Expression { expr: Include { + span: ( + 5, + 1, + ), kind: IncludeOnce, path: LiteralString { value: "bar.php", @@ -17,6 +25,10 @@ }, Expression { expr: Include { + span: ( + 7, + 1, + ), kind: Require, path: LiteralString { value: "baz.php", @@ -25,6 +37,10 @@ }, Expression { expr: Include { + span: ( + 9, + 1, + ), kind: RequireOnce, path: LiteralString { value: "qux.php", diff --git a/tests/fixtures/0010/ast.txt b/tests/fixtures/0010/ast.txt index 938dc94..45aacca 100644 --- a/tests/fixtures/0010/ast.txt +++ b/tests/fixtures/0010/ast.txt @@ -532,6 +532,10 @@ Arg { name: None, value: BitwiseNot { + span: ( + 16, + 13, + ), value: LiteralInteger { i: "2", }, diff --git a/tests/fixtures/0011/ast.txt b/tests/fixtures/0011/ast.txt index a12b702..f42502b 100644 --- a/tests/fixtures/0011/ast.txt +++ b/tests/fixtures/0011/ast.txt @@ -792,6 +792,10 @@ Arg { name: None, value: BitwiseNot { + span: ( + 16, + 13, + ), value: Variable( Variable { start: ( diff --git a/tests/fixtures/0017/ast.txt b/tests/fixtures/0017/ast.txt index 9826458..262823a 100644 --- a/tests/fixtures/0017/ast.txt +++ b/tests/fixtures/0017/ast.txt @@ -50,6 +50,10 @@ ), op: Assign, rhs: UnaryPlus { + span: ( + 4, + 6, + ), value: LiteralInteger { i: "1", }, @@ -73,6 +77,10 @@ ), op: Assign, rhs: BitwiseNot { + span: ( + 5, + 6, + ), value: LiteralInteger { i: "2", }, @@ -96,6 +104,10 @@ ), op: Assign, rhs: PreDecrement { + span: ( + 6, + 6, + ), value: Variable( Variable { start: ( @@ -129,6 +141,10 @@ ), op: Assign, rhs: PreIncrement { + span: ( + 7, + 6, + ), value: Variable( Variable { start: ( diff --git a/tests/fixtures/0072/ast.txt b/tests/fixtures/0072/ast.txt index 298d28a..fa3c292 100644 --- a/tests/fixtures/0072/ast.txt +++ b/tests/fixtures/0072/ast.txt @@ -1,6 +1,10 @@ [ Expression { expr: ErrorSuppress { + span: ( + 1, + 7, + ), expr: Call { target: Identifier( Identifier { diff --git a/tests/fixtures/0103/ast.txt b/tests/fixtures/0103/ast.txt index 11268f8..ffa99b7 100644 --- a/tests/fixtures/0103/ast.txt +++ b/tests/fixtures/0103/ast.txt @@ -1,6 +1,10 @@ [ Expression { expr: Print { + span: ( + 1, + 7, + ), value: Variable( Variable { start: ( diff --git a/tests/fixtures/0237/ast.txt b/tests/fixtures/0237/ast.txt index c1dfd72..97811ca 100644 --- a/tests/fixtures/0237/ast.txt +++ b/tests/fixtures/0237/ast.txt @@ -1,10 +1,18 @@ [ Expression { expr: Include { + span: ( + 3, + 1, + ), kind: Require, path: Infix { lhs: MagicConst { - constant: Dir, + span: ( + 3, + 9, + ), + constant: Directory, }, op: Concat, rhs: LiteralString { diff --git a/tests/fixtures/0238/ast.txt b/tests/fixtures/0238/ast.txt index 4fde4de..a02fd1b 100644 --- a/tests/fixtures/0238/ast.txt +++ b/tests/fixtures/0238/ast.txt @@ -16,10 +16,18 @@ ), op: Assign, rhs: Include { + span: ( + 3, + 8, + ), kind: Require, path: Infix { lhs: MagicConst { - constant: Dir, + span: ( + 3, + 16, + ), + constant: Directory, }, op: Concat, rhs: LiteralString { diff --git a/tests/fixtures/0269/ast.txt b/tests/fixtures/0269/ast.txt index 1b9d943..f14b210 100644 --- a/tests/fixtures/0269/ast.txt +++ b/tests/fixtures/0269/ast.txt @@ -219,6 +219,10 @@ [ Expression { expr: Print { + span: ( + 11, + 5, + ), value: Infix { lhs: Variable( Variable { diff --git a/tests/fixtures/0276 copy/ast.txt b/tests/fixtures/0276 copy/ast.txt new file mode 100644 index 0000000..7399517 --- /dev/null +++ b/tests/fixtures/0276 copy/ast.txt @@ -0,0 +1,74 @@ +[ + Expression { + expr: MagicConst { + span: ( + 3, + 1, + ), + constant: Directory, + }, + }, + Expression { + expr: MagicConst { + span: ( + 4, + 1, + ), + constant: File, + }, + }, + Expression { + expr: MagicConst { + span: ( + 5, + 1, + ), + constant: Line, + }, + }, + Expression { + expr: MagicConst { + span: ( + 6, + 1, + ), + constant: Namespace, + }, + }, + Expression { + expr: MagicConst { + span: ( + 7, + 1, + ), + constant: Class, + }, + }, + Expression { + expr: MagicConst { + span: ( + 8, + 1, + ), + constant: Method, + }, + }, + Expression { + expr: MagicConst { + span: ( + 9, + 1, + ), + constant: Trait, + }, + }, + Expression { + expr: MagicConst { + span: ( + 10, + 1, + ), + constant: Function, + }, + }, +] diff --git a/tests/fixtures/0276 copy/code.php b/tests/fixtures/0276 copy/code.php new file mode 100644 index 0000000..fc7e897 --- /dev/null +++ b/tests/fixtures/0276 copy/code.php @@ -0,0 +1,10 @@ +