Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions Zend/tests/markup_operand_position_bc.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
--TEST--
Native markup: bare "<" after operand-ending tokens stays a comparison (no-space BC)
--FILE--
<?php
// Postfix ++/-- end an operand: "<" is a comparison, not markup.
$i = 0; $n = 5;
$a = [1, 2, 3];
var_dump($i++<PHP_INT_MAX);
var_dump($i--<$n);
$count = 0;
while ($count++<count($a)) {}
var_dump($count);

// The `class` keyword in ::class ends an operand too.
const FOO = "zzz";
var_dump(\stdClass::class<FOO);
var_dump(self_name()<FOO);
function self_name(): string { return "aaa"; }

// Plain constants / qualified names end an operand.
var_dump(FOO<"zzzz");
var_dump(PHP_INT_MAX<FOO);

// `]` (array dereference) and `}` end an operand.
var_dump($a[0]<FOO);
var_dump(match(true) { default => 1 }<FOO);

// A closing heredoc label ends an operand.
var_dump(<<<EOT
aaa
EOT<FOO);

// Interpolated string closing quote ends an operand.
$s = "aa";
var_dump("a$s"<FOO);

// `static` is a complete operand (instanceof static), never markup.
class Base { public function check(object $o): bool { return $o instanceof static<FOO; } }
var_dump((new Base)->check(new Base));

// Bare `exit` is an expression operand; "<" after it is a comparison.
// (Only reached when the left side is falsy-compared, so guard it.)
function never_exit(): bool { return false && exit<FOO; }
var_dump(never_exit());

// The legacy `<>` operator is untouched in infix (operator) position.
var_dump($a[0]<>2);

// Plain variables, literals, and `)` end an operand too, with or without spaces.
$lhs = 1; $rhs = 2;
var_dump($lhs < $rhs);
var_dump($lhs<$rhs);
var_dump(3 < 4);
var_dump(($lhs) < $rhs);

// Other "<"-led operators are unchanged: spaceship, less-or-equal, shifts.
var_dump($lhs <=> $rhs);
var_dump($lhs <= $rhs);
var_dump(1 << 4);
var_dump(64 >> 2);

// A heredoc body containing "<" stays literal text, not markup.
echo <<<EOT
literal <not-markup> text
EOT, "\n";

// Error suppression with "@" still works before an expression.
$sup = []; var_dump(@$sup['missing']);

// Bare `yield` is a complete operand: `<` and `<>` after it stay comparisons
// ((yield) != 5, (yield) < FOO), with or without spaces. Yielding markup
// therefore needs parentheses: `yield (<div/>)`.
function gen_ne() { var_dump(yield <> 5); }
$g = gen_ne(); $g->current(); $g->send(7);
function gen_lt() { var_dump(yield < FOO); }
$g = gen_lt(); $g->current(); $g->send("aaa");
function gen_lt_nospace() { $r = yield<FOO; var_dump($r); }
$g = gen_lt_nospace(); $g->current(); $g->send("aaa");

// Inverse direction: in operand position (after `=`, `(`, `,`, `=>`, `return`,
// and parenthesized after `yield`) a "<tag" IS markup. Compile-time
// classification only - never executed, so this parses even in builds where
// the Html\ runtime classes are unavailable.
function never_runs() {
$x = <div/>;
take(<p>hi</p>, <br/>);
$f = fn() => <span>a</span>;
yield (<li>item</li>);
return <em>done</em>;
}

// Dynamic tags: "<$tag" and "<{expr}" in operand position are markup
// ("$i--<$n" above is the operator-position inverse, still a comparison).
function never_runs_dynamic() {
$tag = 'div';
$x = <$tag class="a">hi</$tag>;
$y = <{$tag . 'x'}>hi</>;
take(<{pick()}/>);
return <$tag/>;
}
echo "parsed\n";
?>
--EXPECT--
bool(true)
bool(true)
int(4)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
int(-1)
bool(true)
int(16)
int(16)
literal <not-markup> text
NULL
bool(true)
bool(true)
bool(true)
parsed
5 changes: 5 additions & 0 deletions Zend/zend_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ struct _zend_php_scanner_globals {
/* initial string length after scanning to first variable */
int scanned_string_len;

/* Last significant token returned, used to decide whether a bare "<" in
* ST_IN_SCRIPTING is in operand position (begins native markup) or operator
* position (a comparison). See zend_language_scanner.l. */
int last_token;

/* hooks */
void (*on_event)(
zend_php_scanner_event event, int token, int line,
Expand Down
66 changes: 66 additions & 0 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "zend_constants.h"
#include "zend_language_scanner.h"
#include "zend_exceptions.h"
#include "zend_markup.h"

#define YYSIZE_T size_t
#define yytnamerr zend_yytnamerr
Expand Down Expand Up @@ -250,6 +251,20 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
/* Token used to force a parse error from the lexer */
%token T_ERROR

/* Native markup tokens. Declared last so they append to the token enum:
* inserting them mid-list would renumber every later token (T_INLINE_HTML
* onward), breaking code that hardcodes token integers. */
%token <ast> T_MARKUP_NAME "markup tag name"
%token <ast> T_MARKUP_TEXT "markup text"
%token <ast> T_MARKUP_ATTR_VALUE "markup attribute value"
%token <ast> T_MARKUP_COMMENT "markup comment"
%token T_MARKUP_OPEN "markup tag start"
%token T_MARKUP_CLOSE_OPEN "markup closing tag start"
%token T_MARKUP_TAG_END "markup tag end"
%token T_MARKUP_SELF_CLOSE "markup self-close"
%token T_MARKUP_INTERP_START "markup interpolation start"
%token <ast> T_MARKUP_DOCTYPE "markup doctype"

%type <ast> top_statement namespace_name name statement function_declaration_statement
%type <ast> class_declaration_statement trait_declaration_statement legacy_namespace_name
%type <ast> interface_declaration_statement interface_extends_list
Expand Down Expand Up @@ -283,6 +298,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%type <ast> attributed_statement attributed_top_statement attributed_class_statement attributed_parameter
%type <ast> attribute_decl attribute attributes attribute_group namespace_declaration_name
%type <ast> match match_arm_list non_empty_match_arm_list match_arm match_arm_cond_list
%type <ast> markup_element markup_children markup_child
%type <ast> markup_attributes markup_attribute
%type <ast> enum_declaration_statement enum_backing_type enum_case enum_case_expr
%type <ast> function_name non_empty_member_modifiers
%type <ast> property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body
Expand Down Expand Up @@ -1249,9 +1266,58 @@ new_non_dereferenceable:
{ $$ = zend_ast_create(ZEND_AST_NEW, $2, zend_ast_create_list(0, ZEND_AST_ARG_LIST)); }
;

markup_element:
T_MARKUP_OPEN T_MARKUP_NAME markup_attributes T_MARKUP_TAG_END markup_children T_MARKUP_CLOSE_OPEN T_MARKUP_NAME T_MARKUP_TAG_END
{ $$ = zend_ast_create_markup_checked($2, $3, $5, $7, false); if (!$$) { YYERROR; } }
| T_MARKUP_OPEN T_MARKUP_NAME markup_attributes T_MARKUP_SELF_CLOSE
{ $$ = zend_ast_create_markup_element($2, $3, NULL); if (!$$) { YYERROR; } }
| T_MARKUP_OPEN T_MARKUP_TAG_END markup_children T_MARKUP_CLOSE_OPEN T_MARKUP_TAG_END
{ $$ = zend_ast_create_markup_element(NULL, NULL, $3); if (!$$) { YYERROR; } }
| T_MARKUP_OPEN T_VARIABLE markup_attributes T_MARKUP_TAG_END markup_children T_MARKUP_CLOSE_OPEN T_VARIABLE T_MARKUP_TAG_END
{ $$ = zend_ast_create_markup_checked($2, $3, $5, $7, true); if (!$$) { YYERROR; } }
| T_MARKUP_OPEN T_VARIABLE markup_attributes T_MARKUP_SELF_CLOSE
{ $$ = zend_ast_create_markup_dynamic($2, $3, NULL); }
| T_MARKUP_OPEN T_MARKUP_INTERP_START expr '}' markup_attributes T_MARKUP_TAG_END markup_children T_MARKUP_CLOSE_OPEN T_MARKUP_TAG_END
{ $$ = zend_ast_create_markup_dynamic_expr($3, $5, $7); }
| T_MARKUP_OPEN T_MARKUP_INTERP_START expr '}' markup_attributes T_MARKUP_SELF_CLOSE
{ $$ = zend_ast_create_markup_dynamic_expr($3, $5, NULL); }
;

markup_attributes:
%empty { $$ = zend_ast_create_list(0, ZEND_AST_ARRAY); }
| markup_attributes markup_attribute { $$ = zend_ast_list_add($1, $2); }
;

markup_attribute:
T_MARKUP_NAME '=' T_MARKUP_ATTR_VALUE
{ $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $3, $1); }
| T_MARKUP_NAME '=' T_MARKUP_INTERP_START expr '}'
{ $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $4, $1); }
| T_MARKUP_NAME
{ zval t; ZVAL_TRUE(&t); $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, zend_ast_create_zval(&t), $1); }
| T_MARKUP_INTERP_START T_ELLIPSIS expr '}'
{ $$ = zend_ast_create(ZEND_AST_UNPACK, $3); }
;

markup_children:
%empty { $$ = zend_ast_create_list(0, ZEND_AST_ARRAY); }
| markup_children markup_child { $$ = zend_ast_list_add($1, $2); }
;

markup_child:
T_MARKUP_TEXT { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $1, NULL); }
| T_MARKUP_COMMENT { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, zend_ast_create_markup_raw($1), NULL); }
| T_MARKUP_DOCTYPE { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, zend_ast_create_markup_raw($1), NULL); }
| T_MARKUP_INTERP_START expr '}' { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $2, NULL); }
| T_MARKUP_INTERP_START '}' { zval n; ZVAL_NULL(&n); $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, zend_ast_create_zval(&n), NULL); }
| markup_element { $$ = zend_ast_create(ZEND_AST_ARRAY_ELEM, $1, NULL); }
;

expr:
variable
{ $$ = $1; }
| markup_element
{ $$ = $1; }
| T_LIST '(' array_pair_list ')' '=' expr
{ $3->attr = ZEND_ARRAY_SYNTAX_LIST; $$ = zend_ast_create(ZEND_AST_ASSIGN, $3, $6); }
| '[' array_pair_list ']' '=' expr
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_language_scanner.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ typedef struct _zend_lex_state {
unsigned char *yy_marker;
unsigned char *yy_limit;
int yy_state;
int last_token; /* operand-vs-operator position for native markup */
zend_stack state_stack;
zend_ptr_stack heredoc_label_stack;
zend_stack nest_location_stack; /* for syntax error reporting */
Expand Down
Loading
Loading