diff --git a/Zend/tests/phpc/001_basic.phpt b/Zend/tests/phpc/001_basic.phpt new file mode 100644 index 000000000000..ea7bb882255f --- /dev/null +++ b/Zend/tests/phpc/001_basic.phpt @@ -0,0 +1,12 @@ +--TEST-- +.phpc: a file without @unlink($file)); +require $file; +?> +--EXPECT-- +hello +3 diff --git a/Zend/tests/phpc/002_php_unchanged.phpt b/Zend/tests/phpc/002_php_unchanged.phpt new file mode 100644 index 000000000000..e0a1439bddce --- /dev/null +++ b/Zend/tests/phpc/002_php_unchanged.phpt @@ -0,0 +1,12 @@ +--TEST-- +.phpc: classic .php behavior is completely unaffected (BC sanity check) +--FILE-- + @unlink($file)); +require $file; +?> +--EXPECT-- +echo "hello\n"; $x = 1 + 2; echo $x, "\n"; diff --git a/Zend/tests/phpc/003_phpc_requires_php.phpt b/Zend/tests/phpc/003_phpc_requires_php.phpt new file mode 100644 index 000000000000..0d4f0a3a506d --- /dev/null +++ b/Zend/tests/phpc/003_phpc_requires_php.phpt @@ -0,0 +1,15 @@ +--TEST-- +.phpc: a pure-PHP file can require a classic .php file +--FILE-- + +--EXPECT-- +hi, world diff --git a/Zend/tests/phpc/004_php_requires_phpc.phpt b/Zend/tests/phpc/004_php_requires_phpc.phpt new file mode 100644 index 000000000000..2be149c00fd0 --- /dev/null +++ b/Zend/tests/phpc/004_php_requires_phpc.phpt @@ -0,0 +1,14 @@ +--TEST-- +.phpc: a classic .php file can require a .phpc file +--FILE-- + @unlink($phpc)); +require $phpc; +echo add(2, 3), "\n"; +?> +--EXPECT-- +5 diff --git a/Zend/tests/phpc/005_utf8_bom.phpt b/Zend/tests/phpc/005_utf8_bom.phpt new file mode 100644 index 000000000000..eb367e21af4d --- /dev/null +++ b/Zend/tests/phpc/005_utf8_bom.phpt @@ -0,0 +1,11 @@ +--TEST-- +.phpc: a leading UTF-8 BOM is silently skipped +--FILE-- + @unlink($file)); +require $file; +?> +--EXPECT-- +bom-ok diff --git a/Zend/tests/phpc/006_halt_compiler.phpt b/Zend/tests/phpc/006_halt_compiler.phpt new file mode 100644 index 000000000000..bae629a88edf --- /dev/null +++ b/Zend/tests/phpc/006_halt_compiler.phpt @@ -0,0 +1,25 @@ +--TEST-- +.phpc: __halt_compiler() works and __COMPILER_HALT_OFFSET__ points to trailing data +--FILE-- + @unlink($file)); +require $file; +?> +--EXPECT-- +before-halt +TRAILING-DATA-12345 diff --git a/Zend/tests/phpc/007_closing_tag.phpt b/Zend/tests/phpc/007_closing_tag.phpt new file mode 100644 index 000000000000..f458ce937c30 --- /dev/null +++ b/Zend/tests/phpc/007_closing_tag.phpt @@ -0,0 +1,13 @@ +--TEST-- +.phpc: ?> in a .phpc file drops to inline output, mirroring .php semantics +--FILE-- +\nplain inline content\n @unlink($file)); +require $file; +?> +--EXPECT-- +from-phpc +plain inline content +back diff --git a/Zend/tests/phpc/008_empty.phpt b/Zend/tests/phpc/008_empty.phpt new file mode 100644 index 000000000000..c82ce5326bec --- /dev/null +++ b/Zend/tests/phpc/008_empty.phpt @@ -0,0 +1,12 @@ +--TEST-- +.phpc: an empty file produces no output and no error +--FILE-- + @unlink($file)); +require $file; +echo "after-require\n"; +?> +--EXPECT-- +after-require diff --git a/Zend/tests/phpc/009_declare_strict_types.phpt b/Zend/tests/phpc/009_declare_strict_types.phpt new file mode 100644 index 000000000000..81745d6772f9 --- /dev/null +++ b/Zend/tests/phpc/009_declare_strict_types.phpt @@ -0,0 +1,11 @@ +--TEST-- +.phpc: declare(strict_types=1) works as first statement in a .phpc file +--FILE-- + @unlink($file)); +require $file; +?> +--EXPECT-- +3 diff --git a/Zend/tests/phpc/010_class_definition.phpt b/Zend/tests/phpc/010_class_definition.phpt new file mode 100644 index 000000000000..01e3ca10635f --- /dev/null +++ b/Zend/tests/phpc/010_class_definition.phpt @@ -0,0 +1,20 @@ +--TEST-- +.phpc: class definitions and namespaces work in a .phpc file +--FILE-- +who}"; } +} + +echo (new Greeter('world'))->hello(), "\n"; +PHPC); +register_shutdown_function(fn() => @unlink($file)); +require $file; +?> +--EXPECT-- +hello, world diff --git a/Zend/tests/phpc/011_eval_unchanged.phpt b/Zend/tests/phpc/011_eval_unchanged.phpt new file mode 100644 index 000000000000..fce79e171a55 --- /dev/null +++ b/Zend/tests/phpc/011_eval_unchanged.phpt @@ -0,0 +1,11 @@ +--TEST-- +.phpc: eval() is unaffected — string compile path is independent of file extension +--FILE-- + +--EXPECT-- +eval-ok diff --git a/Zend/tests/phpc/012_token_get_all_unchanged.phpt b/Zend/tests/phpc/012_token_get_all_unchanged.phpt new file mode 100644 index 000000000000..4a36c039c1cf --- /dev/null +++ b/Zend/tests/phpc/012_token_get_all_unchanged.phpt @@ -0,0 +1,16 @@ +--TEST-- +.phpc: token_get_all() string path is unaffected (still requires +--EXPECT-- +T_OPEN_TAG +T_ECHO +T_WHITESPACE +T_LNUMBER diff --git a/Zend/tests/phpc/013_shebang_main_script.phpt b/Zend/tests/phpc/013_shebang_main_script.phpt new file mode 100644 index 000000000000..fadb9f8c5e6e --- /dev/null +++ b/Zend/tests/phpc/013_shebang_main_script.phpt @@ -0,0 +1,17 @@ +--TEST-- +.phpc: a CLI shebang line at the top of a .phpc main script is silently skipped +--FILE-- + @unlink($file)); + +$php = PHP_BINARY; +$out = shell_exec(escapeshellarg($php) . ' -n ' . escapeshellarg($file)); +echo $out; +?> +--EXPECT-- +shebang-skipped +2 diff --git a/Zend/tests/phpc/014_phpc_with_open_tag.phpt b/Zend/tests/phpc/014_phpc_with_open_tag.phpt new file mode 100644 index 000000000000..88cbc93dceb6 --- /dev/null +++ b/Zend/tests/phpc/014_phpc_with_open_tag.phpt @@ -0,0 +1,18 @@ +--TEST-- +.phpc: an accidental literal ' @unlink($file)); +try { + require $file; +} catch (\ParseError $e) { + echo "ParseError: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +ParseError: syntax error, %s diff --git a/Zend/tests/phpc/015_php_with_phpc_substring.phpt b/Zend/tests/phpc/015_php_with_phpc_substring.phpt new file mode 100644 index 000000000000..bbe2e49b346c --- /dev/null +++ b/Zend/tests/phpc/015_php_with_phpc_substring.phpt @@ -0,0 +1,23 @@ +--TEST-- +.phpc: extension match is strict; ".phpcc" or ".php" with "phpc" in middle are NOT .phpc +--FILE-- + @unlink($f1)); +require $f1; +echo "\n"; + +$f2 = "$dir/{$base}.phpcc"; +file_put_contents($f2, 'echo "must-stay-inline-too";'); +register_shutdown_function(fn() => @unlink($f2)); +require $f2; +?> +--EXPECT-- +echo "must-stay-inline"; +echo "must-stay-inline-too"; diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 07f2d44cb5c6..91122f73c619 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -567,7 +567,58 @@ ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle) zend_error_noreturn(E_COMPILE_ERROR, "zend_stream_mmap() failed"); } - if (CG(skip_shebang)) { + /* Pure-code PHP files via .phpc extension (RFC: optional_php_tags). + * + * Files whose name ends in ".phpc" are parsed as pure PHP: the lexer + * starts directly in ST_IN_SCRIPTING, with no opening "" and __halt_compiler() retain + * their current semantics, so a .phpc file remains free to drop into + * inline output mode or terminate compilation, if desired. + * + * Files whose name does NOT end in ".phpc" (in particular: every .php + * file, every .phar entry, every stream wrapper without a .phpc name, + * every eval()/stdin/highlight invocation) take the historical code + * path unchanged. .phpc detection is purely additive. + */ + bool is_phpc_file = false; + { + zend_string *fname = file_handle->opened_path + ? file_handle->opened_path : file_handle->filename; + if (fname && ZSTR_LEN(fname) >= sizeof(".phpc") - 1) { + const char *tail = ZSTR_VAL(fname) + ZSTR_LEN(fname) - (sizeof(".phpc") - 1); + if (memcmp(tail, ".phpc", sizeof(".phpc") - 1) == 0) { + is_phpc_file = true; + } + } + } + + uint32_t phpc_start_lineno = 1; + if (is_phpc_file) { + unsigned char *p = (unsigned char *)buf; + unsigned char *limit = p + size; + + /* Skip a UTF-8 BOM if present. */ + if (limit - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) { + p += 3; + } + + /* Skip a shebang line in CLI mode. .phpc files can be #!-scripts. */ + if (CG(skip_shebang) && limit - p >= 2 && p[0] == '#' && p[1] == '!') { + while (p < limit && *p != '\n') { + p++; + } + if (p < limit) { + p++; /* consume the newline */ + phpc_start_lineno = 2; + } + } + + if (p != (unsigned char *)buf) { + SCNG(yy_cursor) = p; + } + BEGIN(ST_IN_SCRIPTING); + } else if (CG(skip_shebang)) { BEGIN(SHEBANG); } else { BEGIN(INITIAL); @@ -585,7 +636,7 @@ ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle) SCNG(on_event) = NULL; SCNG(on_event_context) = NULL; RESET_DOC_COMMENT(); - CG(zend_lineno) = 1; + CG(zend_lineno) = phpc_start_lineno; CG(increment_lineno) = 0; return SUCCESS; }