Compare commits

..

14 Commits

Author SHA1 Message Date
Nicolas Grekas
1172dc1abe [Debug] workaround BC break in PHP 7.3 2019-06-18 23:26:03 +02:00
Fabien Potencier
7e1a4ec082 fixed CS 2019-06-13 12:34:15 +02:00
Alexander M. Turek
e79bbe15d8 Use willReturn() instead of will(returnValue()). 2019-05-30 17:47:52 +02:00
Grégoire Pineau
671fc55bd1 [Debug] Wrap call to require_once in a try/catch
If the included file contains an error, it hides the real error. This
makes debugging harder.

How to reproduce:

```
composer create-project symfony/skeleton symfony-3.4 3.4
cd symfony-3.4
composer req monolog
```

Add to `monolog.yaml`:
```yaml
        elasticsearch:
            type: "elasticsearch"
            elasticsearch:
                host: 'elasticsearch'
                port: '9200'
            index: 'ep_php_logs_dev'
            level: 'debug'
            tags: 'monolog.logger'
            channels: ['!event']
```

This will fail because the the \Elastica\Client class does not exist.
But this error will be hidden by the `ClassNotFoundFatalErrorHandler`
because it will try to load the `Symfony\Component\Kernel\Client` and
this class extends `Symfony\Component\BrowserKit\Client`. The last one
is a soft dependency...

---

Before
```
Fatal error: Uncaught Error: Class 'Symfony\Component\BrowserKit\Client' not found in /tmp/symfony-3.4/vendor/symfony/http-kernel/Client.php:31
```

After:
```
Fatal error: Uncaught Symfony\Component\Debug\Exception\ClassNotFoundException: Attempted to load class "Client" from namespace "Elastica".
Did you forget a "use" statement for another namespace? in /tmp/symfony-es/var/cache/dev/ContainerWXN4mS9/srcApp_KernelDevDebugContainer.php:303
```
2019-05-18 15:32:47 +02:00
Martijn Cuppens
681afbb264 Optimize SVGs 2019-04-11 11:48:14 +02:00
Grégoire Pineau
63b4ddb7c6 [Debug] Fixed error handling when an error is already handled when another error is already handled (5) 2019-04-07 14:43:40 +02:00
Grégoire Paris
032e6624c3 Use a class name that does not actually exist
Using "Foo", a class name that corresponds to no less than 22 fixture
classes, results in the first found "Foo" being loaded when one is
found by the ClassNotFoundFatalErrorHandler error handler, I am not sure
exactly why, but it is not really a big issue because this is a fatal
error handler and execution is not supposed to continue after that.
Except that is very much the case when running the whole test suite
sequentially with ./phpunit . Then we arrive to the DI component test
suite, and a failure happens because \\foo was not supposed to be defined:

> Failed asserting that exception message 'The definition for "\foo" has
> no class attribute, and appears to reference a class or interface in the
> global namespace. Leaving out the "class" attribute is only allowed for
> namespaced classes. Please specify the class attribute explicitly to get
> rid of this error.' contains 'The definition for "\foo" has no class.'.
2019-04-06 21:25:09 +02:00
Nicolas Grekas
adbdd5d663 [TwigBridge] remove deprecation triggered when using Twig 2.7 2019-03-10 18:07:42 +01:00
Thomas Calvet
a5961253fa [Debug][DebugClassLoader] Detect annotations before blank docblock lines on final and internal methods 2019-03-06 15:53:23 +01:00
Christian Flothmann
878d05a2eb detect annotations before blank docblock lines 2019-03-04 11:54:25 +01:00
Fabien Potencier
d5f9980171 fixed CS 2019-03-04 10:11:50 +01:00
Pascal Luna
8d8a9e877b Fixed the DebugClassLoader compatibility with eval()'d code on Darwin 2019-02-24 16:45:11 +01:00
Nicolas Grekas
1fe36a98e2 Apply php-cs-fixer rule for array_key_exists() 2019-02-23 16:06:07 +01:00
Thomas Calvet
8b8b8b7e57 [Debug][ErrorHandler] Preserve next error handler
Co-authored-by: Joe <cuchac@email.cz>
2019-02-16 11:38:56 +01:00
11 changed files with 122 additions and 42 deletions

View File

@@ -168,7 +168,7 @@ class DebugClassLoader
private function checkClass($class, $file = null)
{
$exists = null === $file || \class_exists($class, false) || \interface_exists($class, false) || \trait_exists($class, false);
$exists = null === $file || class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false);
if (null !== $file && $class && '\\' === $class[0]) {
$class = substr($class, 1);
@@ -186,13 +186,13 @@ class DebugClassLoader
}
$name = $refl->getName();
if ($name !== $class && 0 === \strcasecmp($name, $class)) {
if ($name !== $class && 0 === strcasecmp($name, $class)) {
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name));
}
$deprecations = $this->checkAnnotations($refl, $name);
if (isset(self::$php7Reserved[\strtolower($refl->getShortName())])) {
if (isset(self::$php7Reserved[strtolower($refl->getShortName())])) {
$deprecations[] = sprintf('The "%s" class uses the reserved name "%s", it will break on PHP 7 and higher', $name, $refl->getShortName());
}
@@ -223,23 +223,23 @@ class DebugClassLoader
$deprecations = [];
// Don't trigger deprecations for classes in the same vendor
if (2 > $len = 1 + (\strpos($class, '\\') ?: \strpos($class, '_'))) {
if (2 > $len = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) {
$len = 0;
$ns = '';
} else {
$ns = \str_replace('_', '\\', \substr($class, 0, $len));
$ns = str_replace('_', '\\', substr($class, 0, $len));
}
// Detect annotations on the class
if (false !== $doc = $refl->getDocComment()) {
foreach (['final', 'deprecated', 'internal'] as $annotation) {
if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
if (false !== strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$|\r?\n)#s', $doc, $notice)) {
self::${$annotation}[$class] = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : '';
}
}
}
$parent = \get_parent_class($class);
$parent = get_parent_class($class);
$parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent);
if ($parent) {
$parentAndOwnInterfaces[$parent] = $parent;
@@ -254,22 +254,22 @@ class DebugClassLoader
}
// Detect if the parent is annotated
foreach ($parentAndOwnInterfaces + \class_uses($class, false) as $use) {
foreach ($parentAndOwnInterfaces + class_uses($class, false) as $use) {
if (!isset(self::$checkedClasses[$use])) {
$this->checkClass($use);
}
if (isset(self::$deprecated[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
if (isset(self::$deprecated[$use]) && strncmp($ns, str_replace('_', '\\', $use), $len) && !isset(self::$deprecated[$class])) {
$type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
$verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
$deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $class, $type, $verb, $use, self::$deprecated[$use]);
}
if (isset(self::$internal[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
if (isset(self::$internal[$use]) && strncmp($ns, str_replace('_', '\\', $use), $len)) {
$deprecations[] = sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $class);
}
}
if (\trait_exists($class)) {
if (trait_exists($class)) {
return $deprecations;
}
@@ -296,7 +296,7 @@ class DebugClassLoader
if (isset(self::$internalMethods[$class][$method->name])) {
list($declaringClass, $message) = self::$internalMethods[$class][$method->name];
if (\strncmp($ns, $declaringClass, $len)) {
if (strncmp($ns, $declaringClass, $len)) {
$deprecations[] = sprintf('The "%s::%s()" method is considered internal%s. It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $class);
}
}
@@ -307,7 +307,7 @@ class DebugClassLoader
}
foreach (['final', 'internal'] as $annotation) {
if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
if (false !== strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$|\r?\n)#s', $doc, $notice)) {
$message = isset($notice[1]) ? preg_replace('#\.?\r?\n( \*)? *(?= |\r?\n|$)#', '', $notice[1]) : '';
self::${$annotation.'Methods'}[$class][$method->name] = [$class, $message];
}
@@ -390,6 +390,11 @@ class DebugClassLoader
$dirFiles = self::$darwinCache[$kDir][1];
if (!isset($dirFiles[$file]) && ') : eval()\'d code' === substr($file, -17)) {
// Get the file name from "file_name.php(123) : eval()'d code"
$file = substr($file, 0, strrpos($file, '(', -17));
}
if (isset($dirFiles[$file])) {
return $real .= $dirFiles[$file];
}

View File

@@ -222,7 +222,7 @@ class ErrorHandler
}
if (!\is_array($log)) {
$log = [$log];
} elseif (!array_key_exists(0, $log)) {
} elseif (!\array_key_exists(0, $log)) {
throw new \InvalidArgumentException('No logger provided');
}
if (null === $log[0]) {
@@ -382,6 +382,11 @@ class ErrorHandler
*/
public function handleError($type, $message, $file, $line)
{
// @deprecated to be removed in Symfony 5.0
if (\PHP_VERSION_ID >= 70300 && $message && '"' === $message[0] && 0 === strpos($message, '"continue') && preg_match('/^"continue(?: \d++)?" targeting switch is equivalent to "break(?: \d++)?"\. Did you mean to use "continue(?: \d++)?"\?$/', $message)) {
$type = E_DEPRECATED;
}
// Level is the current error reporting level to manage silent error.
$level = error_reporting();
$silenced = 0 === ($level & $type);
@@ -517,6 +522,11 @@ class ErrorHandler
$errorAsException ? ['exception' => $errorAsException] : [],
];
} else {
if (!\defined('HHVM_VERSION')) {
$currentErrorHandler = set_error_handler('var_dump');
restore_error_handler();
}
try {
$this->isRecursive = true;
$level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG;
@@ -525,7 +535,7 @@ class ErrorHandler
$this->isRecursive = false;
if (!\defined('HHVM_VERSION')) {
set_error_handler([$this, __FUNCTION__]);
set_error_handler($currentErrorHandler);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -185,7 +185,11 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
}
}
require_once $file;
try {
require_once $file;
} catch (\Throwable $e) {
return null;
}
foreach ($candidates as $candidate) {
if ($this->classExists($candidate)) {

View File

@@ -297,7 +297,7 @@ class DebugClassLoaderTest extends TestCase
require __DIR__.'/Fixtures/FinalClasses.php';
$i = 1;
while(class_exists($finalClass = __NAMESPACE__.'\\Fixtures\\FinalClass'.$i++, false)) {
while (class_exists($finalClass = __NAMESPACE__.'\\Fixtures\\FinalClass'.$i++, false)) {
spl_autoload_call($finalClass);
class_exists('Test\\'.__NAMESPACE__.'\\Extends'.substr($finalClass, strrpos($finalClass, '\\') + 1), true);
}
@@ -385,6 +385,11 @@ class DebugClassLoaderTest extends TestCase
$this->assertSame([], $deprecations);
}
public function testEvaluatedCode()
{
$this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\DefinitionInEvaluatedCode', true));
}
}
class ClassLoader

View File

@@ -12,12 +12,12 @@
namespace Symfony\Component\Debug\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use Symfony\Component\Debug\BufferingLogger;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\Exception\SilencedErrorContext;
use Symfony\Component\Debug\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne;
use Symfony\Component\Debug\Tests\Fixtures\LoggerThatSetAnErrorHandler;
/**
@@ -234,7 +234,7 @@ class ErrorHandlerTest extends TestCase
$logger
->expects($this->once())
->method('log')
->will($this->returnCallback($warnArgCheck))
->willReturnCallback($warnArgCheck)
;
$handler = ErrorHandler::register();
@@ -262,7 +262,7 @@ class ErrorHandlerTest extends TestCase
$logger
->expects($this->once())
->method('log')
->will($this->returnCallback($logArgCheck))
->willReturnCallback($logArgCheck)
;
$handler = ErrorHandler::register();
@@ -318,7 +318,7 @@ class ErrorHandlerTest extends TestCase
$logger
->expects($this->once())
->method('log')
->will($this->returnCallback($logArgCheck))
->willReturnCallback($logArgCheck)
;
$handler = new ErrorHandler();
@@ -349,7 +349,7 @@ class ErrorHandlerTest extends TestCase
$logger
->expects($this->exactly(2))
->method('log')
->will($this->returnCallback($logArgCheck))
->willReturnCallback($logArgCheck)
;
$handler->setDefaultLogger($logger, E_ERROR);
@@ -503,7 +503,7 @@ class ErrorHandlerTest extends TestCase
$logger
->expects($this->once())
->method('log')
->will($this->returnCallback($logArgCheck))
->willReturnCallback($logArgCheck)
;
$handler->setDefaultLogger($logger, E_PARSE);
@@ -525,7 +525,7 @@ class ErrorHandlerTest extends TestCase
*/
public function testHandleErrorException()
{
$exception = new \Error("Class 'Foo' not found");
$exception = new \Error("Class 'IReallyReallyDoNotExistAnywhereInTheRepositoryISwear' not found");
$handler = new ErrorHandler();
$handler->setExceptionHandler(function () use (&$args) {
@@ -535,7 +535,7 @@ class ErrorHandlerTest extends TestCase
$handler->handleException($exception);
$this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $args[0]);
$this->assertStringStartsWith("Attempted to load class \"Foo\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage());
$this->assertStringStartsWith("Attempted to load class \"IReallyReallyDoNotExistAnywhereInTheRepositoryISwear\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage());
}
/**
@@ -590,26 +590,40 @@ class ErrorHandlerTest extends TestCase
}
/**
* @dataProvider errorHandlerIsNotLostWhenLoggingProvider
* @dataProvider errorHandlerWhenLoggingProvider
*/
public function testErrorHandlerIsNotLostWhenLogging($customErrorHandlerHasBeenPreviouslyDefined, LoggerInterface $logger)
public function testErrorHandlerWhenLogging($previousHandlerWasDefined, $loggerSetsAnotherHandler, $nextHandlerIsDefined)
{
try {
if ($customErrorHandlerHasBeenPreviouslyDefined) {
if ($previousHandlerWasDefined) {
set_error_handler('count');
}
$logger = $loggerSetsAnotherHandler ? new LoggerThatSetAnErrorHandler() : new NullLogger();
$handler = ErrorHandler::register();
$handler->setDefaultLogger($logger);
if ($nextHandlerIsDefined) {
$handler = ErrorHandlerThatUsesThePreviousOne::register();
}
@trigger_error('foo', E_USER_DEPRECATED);
@trigger_error('bar', E_USER_DEPRECATED);
$this->assertSame([$handler, 'handleError'], set_error_handler('var_dump'));
if ($logger instanceof LoggerThatSetAnErrorHandler) {
$this->assertCount(2, $logger->cleanLogs());
}
restore_error_handler();
if ($customErrorHandlerHasBeenPreviouslyDefined) {
if ($previousHandlerWasDefined) {
restore_error_handler();
}
if ($nextHandlerIsDefined) {
restore_error_handler();
}
} finally {
@@ -618,13 +632,14 @@ class ErrorHandlerTest extends TestCase
}
}
public function errorHandlerIsNotLostWhenLoggingProvider()
public function errorHandlerWhenLoggingProvider()
{
return [
[false, new NullLogger()],
[true, new NullLogger()],
[false, new LoggerThatSetAnErrorHandler()],
[true, new LoggerThatSetAnErrorHandler()],
];
foreach ([false, true] as $previousHandlerWasDefined) {
foreach ([false, true] as $loggerSetsAnotherHandler) {
foreach ([false, true] as $nextHandlerIsDefined) {
yield [$previousHandlerWasDefined, $loggerSetsAnotherHandler, $nextHandlerIsDefined];
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
eval('
namespace Symfony\Component\Debug\Tests\Fixtures;
class DefinitionInEvaluatedCode
{
}
');

View File

@@ -0,0 +1,22 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
class ErrorHandlerThatUsesThePreviousOne
{
private static $previous;
public static function register()
{
$handler = new static();
self::$previous = set_error_handler([$handler, 'handleError']);
return $handler;
}
public function handleError($type, $message, $file, $line, $context)
{
return \call_user_func(self::$previous, $type, $message, $file, $line, $context);
}
}

View File

@@ -30,6 +30,7 @@ class FinalClass3
/**
* @final
*
* @author John Doe
*/
class FinalClass4
@@ -63,7 +64,6 @@ class FinalClass6
* @author John Doe
*
* @final another
*
* multiline comment...
*
* @return string
@@ -76,6 +76,7 @@ class FinalClass7
/**
* @author John Doe
* @final
*
* @return string
*/
class FinalClass8

View File

@@ -13,6 +13,8 @@ class FinalMethod
/**
* @final
*
* @return int
*/
public function finalMethod2()
{

View File

@@ -2,13 +2,14 @@
namespace Symfony\Component\Debug\Tests\Fixtures;
use Psr\Log\AbstractLogger;
use Symfony\Component\Debug\BufferingLogger;
class LoggerThatSetAnErrorHandler extends AbstractLogger
class LoggerThatSetAnErrorHandler extends BufferingLogger
{
public function log($level, $message, array $context = [])
{
set_error_handler('is_string');
parent::log($level, $message, $context);
restore_error_handler();
}
}