Skip to content

Commit 199bfc5

Browse files
PHP 8.4 compatibility - closes #4588
1 parent 5ea7d04 commit 199bfc5

17 files changed

+3929
-2
lines changed

composer.json

+25-2
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,14 @@
200200
"Whoops\\Handler\\": "overrides/filp/whoops/src/Whoops/Handler/",
201201
"Whoops\\Util\\": "overrides/filp/whoops/src/Whoops/Util/",
202202
"Mews\\Purifier\\": "overrides/mews/purifier/src/",
203-
"Cron\\": "overrides/mtdowling/cron-expression/src/Cron/"
203+
"Cron\\": "overrides/mtdowling/cron-expression/src/Cron/",
204+
"Psy\\": "overrides/psy/psysh/src/",
205+
"Psy\\Input\\": "overrides/psy/psysh/src/Input/",
206+
"Psy\\Output\\": "overrides/psy/psysh/src/Output/",
207+
"Psy\\Command\\": "overrides/psy/psysh/src/Command/",
208+
"Psy\\TimeitCommand\\": "overrides/psy/psysh/src/TimeitCommand/",
209+
"Psy\\Command\\ListCommand\\": "overrides/psy/psysh/src/Command/ListCommand/",
210+
"Psy\\Exception\\": "overrides/psy/psysh/src/ErrorException/"
204211
},
205212
"exclude-from-classmap": [
206213
"Modules/",
@@ -362,6 +369,7 @@
362369
"vendor/rap2hpoutre/laravel-log-viewer/src/controllers/LogViewerController.php",
363370
"vendor/symfony/console/Descriptor/TextDescriptor.php",
364371
"vendor/symfony/console/Input/ArrayInput.php",
372+
"vendor/symfony/console/Input/StringInput.php",
365373
"vendor/symfony/console/Input/Input.php",
366374
"vendor/symfony/console/Helper/Helper.php",
367375
"vendor/symfony/console/Helper/ProcessHelper.php",
@@ -447,7 +455,22 @@
447455
"vendor/laravel/framework/src/Illuminate/Validation/Factory.php",
448456
"vendor/laravel/framework/src/Illuminate/Validation/ValidationRuleParser.php",
449457
"vendor/laravel/framework/src/Illuminate/Foundation/Validation/ValidatesRequests.php",
450-
"vendor/chumper/zipper/src/Chumper/Zipper/Zipper.php"
458+
"vendor/chumper/zipper/src/Chumper/Zipper/Zipper.php",
459+
"vendor/psy/psysh/src/Shell.php",
460+
"vendor/psy/psysh/src/CodeCleaner.php",
461+
"vendor/psy/psysh/src/Input/FilterOptions.php",
462+
"vendor/psy/psysh/src/Input/ShellInput.php",
463+
"vendor/psy/psysh/src/Output/ShellOutput.php",
464+
"vendor/psy/psysh/src/Command/Command.php",
465+
"vendor/psy/psysh/src/Command/ListCommand.php",
466+
"vendor/psy/psysh/src/Command/TimeitCommand/TimeitVisitor.php",
467+
"vendor/psy/psysh/src/Command/ListCommand/ClassConstantEnumerator.php",
468+
"vendor/psy/psysh/src/Command/ListCommand/Enumerator.php",
469+
"vendor/psy/psysh/src/Command/ListCommand/ClassEnumerator.php",
470+
"vendor/psy/psysh/src/Command/ListCommand/ConstantEnumerator.php",
471+
"vendor/psy/psysh/src/Command/ListCommand/FunctionEnumerator.php",
472+
"vendor/psy/psysh/src/Exception/ErrorException.php",
473+
"vendor/psy/psysh/src/Exception/BreakException.php"
451474
]
452475
},
453476
"autoload-dev": {
+349
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Psy Shell.
5+
*
6+
* (c) 2012-2018 Justin Hileman
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Psy;
13+
14+
use PhpParser\NodeTraverser;
15+
use PhpParser\Parser;
16+
use PhpParser\PrettyPrinter\Standard as Printer;
17+
use Psy\CodeCleaner\AbstractClassPass;
18+
use Psy\CodeCleaner\AssignThisVariablePass;
19+
use Psy\CodeCleaner\CalledClassPass;
20+
use Psy\CodeCleaner\CallTimePassByReferencePass;
21+
use Psy\CodeCleaner\ExitPass;
22+
use Psy\CodeCleaner\FinalClassPass;
23+
use Psy\CodeCleaner\FunctionContextPass;
24+
use Psy\CodeCleaner\FunctionReturnInWriteContextPass;
25+
use Psy\CodeCleaner\ImplicitReturnPass;
26+
use Psy\CodeCleaner\InstanceOfPass;
27+
use Psy\CodeCleaner\LeavePsyshAlonePass;
28+
use Psy\CodeCleaner\LegacyEmptyPass;
29+
use Psy\CodeCleaner\ListPass;
30+
use Psy\CodeCleaner\LoopContextPass;
31+
use Psy\CodeCleaner\MagicConstantsPass;
32+
use Psy\CodeCleaner\NamespacePass;
33+
use Psy\CodeCleaner\PassableByReferencePass;
34+
use Psy\CodeCleaner\RequirePass;
35+
use Psy\CodeCleaner\StrictTypesPass;
36+
use Psy\CodeCleaner\UseStatementPass;
37+
use Psy\CodeCleaner\ValidClassNamePass;
38+
use Psy\CodeCleaner\ValidConstantPass;
39+
use Psy\CodeCleaner\ValidConstructorPass;
40+
use Psy\CodeCleaner\ValidFunctionNamePass;
41+
use Psy\Exception\ParseErrorException;
42+
43+
/**
44+
* A service to clean up user input, detect parse errors before they happen,
45+
* and generally work around issues with the PHP code evaluation experience.
46+
*/
47+
class CodeCleaner
48+
{
49+
private $parser;
50+
private $printer;
51+
private $traverser;
52+
private $namespace;
53+
54+
/**
55+
* CodeCleaner constructor.
56+
*
57+
* @param Parser $parser A PhpParser Parser instance. One will be created if not explicitly supplied
58+
* @param Printer $printer A PhpParser Printer instance. One will be created if not explicitly supplied
59+
* @param NodeTraverser $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
60+
*/
61+
public function __construct(?Parser $parser = null, ?Printer $printer = null, ?NodeTraverser $traverser = null)
62+
{
63+
if ($parser === null) {
64+
$parserFactory = new ParserFactory();
65+
$parser = $parserFactory->createParser();
66+
}
67+
68+
$this->parser = $parser;
69+
$this->printer = $printer ?: new Printer();
70+
$this->traverser = $traverser ?: new NodeTraverser();
71+
72+
foreach ($this->getDefaultPasses() as $pass) {
73+
$this->traverser->addVisitor($pass);
74+
}
75+
}
76+
77+
/**
78+
* Get default CodeCleaner passes.
79+
*
80+
* @return array
81+
*/
82+
private function getDefaultPasses()
83+
{
84+
$useStatementPass = new UseStatementPass();
85+
$namespacePass = new NamespacePass($this);
86+
87+
// Try to add implicit `use` statements and an implicit namespace,
88+
// based on the file in which the `debug` call was made.
89+
$this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
90+
91+
return [
92+
// Validation passes
93+
new AbstractClassPass(),
94+
new AssignThisVariablePass(),
95+
new CalledClassPass(),
96+
new CallTimePassByReferencePass(),
97+
new FinalClassPass(),
98+
new FunctionContextPass(),
99+
new FunctionReturnInWriteContextPass(),
100+
new InstanceOfPass(),
101+
new LeavePsyshAlonePass(),
102+
new LegacyEmptyPass(),
103+
new ListPass(),
104+
new LoopContextPass(),
105+
new PassableByReferencePass(),
106+
new ValidConstructorPass(),
107+
108+
// Rewriting shenanigans
109+
$useStatementPass, // must run before the namespace pass
110+
new ExitPass(),
111+
new ImplicitReturnPass(),
112+
new MagicConstantsPass(),
113+
$namespacePass, // must run after the implicit return pass
114+
new RequirePass(),
115+
new StrictTypesPass(),
116+
117+
// Namespace-aware validation (which depends on aforementioned shenanigans)
118+
new ValidClassNamePass(),
119+
new ValidConstantPass(),
120+
new ValidFunctionNamePass(),
121+
];
122+
}
123+
124+
/**
125+
* "Warm up" code cleaner passes when we're coming from a debug call.
126+
*
127+
* This is useful, for example, for `UseStatementPass` and `NamespacePass`
128+
* which keep track of state between calls, to maintain the current
129+
* namespace and a map of use statements.
130+
*
131+
* @param array $passes
132+
*/
133+
private function addImplicitDebugContext(array $passes)
134+
{
135+
$file = $this->getDebugFile();
136+
if ($file === null) {
137+
return;
138+
}
139+
140+
try {
141+
$code = @\file_get_contents($file);
142+
if (!$code) {
143+
return;
144+
}
145+
146+
$stmts = $this->parse($code, true);
147+
if ($stmts === false) {
148+
return;
149+
}
150+
151+
// Set up a clean traverser for just these code cleaner passes
152+
$traverser = new NodeTraverser();
153+
foreach ($passes as $pass) {
154+
$traverser->addVisitor($pass);
155+
}
156+
157+
$traverser->traverse($stmts);
158+
} catch (\Throwable $e) {
159+
// Don't care.
160+
} catch (\Exception $e) {
161+
// Still don't care.
162+
}
163+
}
164+
165+
/**
166+
* Search the stack trace for a file in which the user called Psy\debug.
167+
*
168+
* @return string|null
169+
*/
170+
private static function getDebugFile()
171+
{
172+
$trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
173+
174+
foreach (\array_reverse($trace) as $stackFrame) {
175+
if (!self::isDebugCall($stackFrame)) {
176+
continue;
177+
}
178+
179+
if (\preg_match('/eval\(/', $stackFrame['file'])) {
180+
\preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches);
181+
182+
return $matches[1][0];
183+
}
184+
185+
return $stackFrame['file'];
186+
}
187+
}
188+
189+
/**
190+
* Check whether a given backtrace frame is a call to Psy\debug.
191+
*
192+
* @param array $stackFrame
193+
*
194+
* @return bool
195+
*/
196+
private static function isDebugCall(array $stackFrame)
197+
{
198+
$class = isset($stackFrame['class']) ? $stackFrame['class'] : null;
199+
$function = isset($stackFrame['function']) ? $stackFrame['function'] : null;
200+
201+
return ($class === null && $function === 'Psy\debug') ||
202+
($class === 'Psy\Shell' && $function === 'debug');
203+
}
204+
205+
/**
206+
* Clean the given array of code.
207+
*
208+
* @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
209+
*
210+
* @param array $codeLines
211+
* @param bool $requireSemicolons
212+
*
213+
* @return string|false Cleaned PHP code, False if the input is incomplete
214+
*/
215+
public function clean(array $codeLines, $requireSemicolons = false)
216+
{
217+
$stmts = $this->parse('<?php ' . \implode(PHP_EOL, $codeLines) . PHP_EOL, $requireSemicolons);
218+
if ($stmts === false) {
219+
return false;
220+
}
221+
222+
// Catch fatal errors before they happen
223+
$stmts = $this->traverser->traverse($stmts);
224+
225+
// Work around https://github.com/nikic/PHP-Parser/issues/399
226+
$oldLocale = \setlocale(LC_NUMERIC, 0);
227+
\setlocale(LC_NUMERIC, 'C');
228+
229+
$code = $this->printer->prettyPrint($stmts);
230+
231+
// Now put the locale back
232+
\setlocale(LC_NUMERIC, $oldLocale);
233+
234+
return $code;
235+
}
236+
237+
/**
238+
* Set the current local namespace.
239+
*
240+
* @param null|array $namespace (default: null)
241+
*
242+
* @return null|array
243+
*/
244+
public function setNamespace(?array $namespace = null)
245+
{
246+
$this->namespace = $namespace;
247+
}
248+
249+
/**
250+
* Get the current local namespace.
251+
*
252+
* @return null|array
253+
*/
254+
public function getNamespace()
255+
{
256+
return $this->namespace;
257+
}
258+
259+
/**
260+
* Lex and parse a block of code.
261+
*
262+
* @see Parser::parse
263+
*
264+
* @throws ParseErrorException for parse errors that can't be resolved by
265+
* waiting a line to see what comes next
266+
*
267+
* @param string $code
268+
* @param bool $requireSemicolons
269+
*
270+
* @return array|false A set of statements, or false if incomplete
271+
*/
272+
protected function parse($code, $requireSemicolons = false)
273+
{
274+
try {
275+
return $this->parser->parse($code);
276+
} catch (\PhpParser\Error $e) {
277+
if ($this->parseErrorIsUnclosedString($e, $code)) {
278+
return false;
279+
}
280+
281+
if ($this->parseErrorIsUnterminatedComment($e, $code)) {
282+
return false;
283+
}
284+
285+
if ($this->parseErrorIsTrailingComma($e, $code)) {
286+
return false;
287+
}
288+
289+
if (!$this->parseErrorIsEOF($e)) {
290+
throw ParseErrorException::fromParseError($e);
291+
}
292+
293+
if ($requireSemicolons) {
294+
return false;
295+
}
296+
297+
try {
298+
// Unexpected EOF, try again with an implicit semicolon
299+
return $this->parser->parse($code . ';');
300+
} catch (\PhpParser\Error $e) {
301+
return false;
302+
}
303+
}
304+
}
305+
306+
private function parseErrorIsEOF(\PhpParser\Error $e)
307+
{
308+
$msg = $e->getRawMessage();
309+
310+
return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false);
311+
}
312+
313+
/**
314+
* A special test for unclosed single-quoted strings.
315+
*
316+
* Unlike (all?) other unclosed statements, single quoted strings have
317+
* their own special beautiful snowflake syntax error just for
318+
* themselves.
319+
*
320+
* @param \PhpParser\Error $e
321+
* @param string $code
322+
*
323+
* @return bool
324+
*/
325+
private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
326+
{
327+
if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
328+
return false;
329+
}
330+
331+
try {
332+
$this->parser->parse($code . "';");
333+
} catch (\Exception $e) {
334+
return false;
335+
}
336+
337+
return true;
338+
}
339+
340+
private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
341+
{
342+
return $e->getRawMessage() === 'Unterminated comment';
343+
}
344+
345+
private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code)
346+
{
347+
return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ',');
348+
}
349+
}

0 commit comments

Comments
 (0)