diff --git a/Cloner/Internal/NoDefault.php b/Cloner/Internal/NoDefault.php new file mode 100644 index 00000000..ed9db988 --- /dev/null +++ b/Cloner/Internal/NoDefault.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Cloner\Internal; + +/** + * Flags a typed property that has no default value. + * + * This dummy object is used to distinguish a property with a default value of null + * from a property that is uninitialized by default. + * + * @internal + */ +enum NoDefault +{ + case NoDefault; +} diff --git a/Cloner/Stub.php b/Cloner/Stub.php index 0c2a4b9d..a377d2b9 100644 --- a/Cloner/Stub.php +++ b/Cloner/Stub.php @@ -11,6 +11,8 @@ namespace Symfony\Component\VarDumper\Cloner; +use Symfony\Component\VarDumper\Cloner\Internal\NoDefault; + /** * Represents the main properties of a PHP variable. * @@ -50,15 +52,20 @@ public function __sleep(): array $properties = []; if (!isset(self::$defaultProperties[$c = static::class])) { - self::$defaultProperties[$c] = get_class_vars($c); + $reflection = new \ReflectionClass($c); + self::$defaultProperties[$c] = []; + + foreach ($reflection->getProperties() as $p) { + if ($p->isStatic()) { + continue; + } - foreach ((new \ReflectionClass($c))->getStaticProperties() as $k => $v) { - unset(self::$defaultProperties[$c][$k]); + self::$defaultProperties[$c][$p->name] = $p->hasDefaultValue() ? $p->getDefaultValue() : ($p->hasType() ? NoDefault::NoDefault : null); } } foreach (self::$defaultProperties[$c] as $k => $v) { - if ($this->$k !== $v) { + if (NoDefault::NoDefault === $v || $this->$k !== $v) { $properties[] = $k; } } diff --git a/Dumper/CliDumper.php b/Dumper/CliDumper.php index af963707..5bd42529 100644 --- a/Dumper/CliDumper.php +++ b/Dumper/CliDumper.php @@ -11,6 +11,7 @@ namespace Symfony\Component\VarDumper\Dumper; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\VarDumper\Cloner\Cursor; use Symfony\Component\VarDumper\Cloner\Stub; @@ -82,7 +83,7 @@ public function __construct($output = null, ?string $charset = null, int $flags ]); } - $this->displayOptions['fileLinkFormat'] = \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l'; + $this->displayOptions['fileLinkFormat'] = class_exists(FileLinkFormatter::class) ? new FileLinkFormatter() : (\ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l'); } /** @@ -624,19 +625,30 @@ private function hasColorSupport(mixed $stream): bool return false; } - if ('Hyper' === getenv('TERM_PROGRAM')) { + // Detect msysgit/mingw and assume this is a tty because detection + // does not work correctly, see https://github.com/composer/composer/issues/9690 + if (!@stream_isatty($stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($stream)) { + return true; + } + + if ('Hyper' === getenv('TERM_PROGRAM') + || false !== getenv('COLORTERM') + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + ) { return true; } - if (\DIRECTORY_SEPARATOR === '\\') { - return (\function_exists('sapi_windows_vt100_support') - && @sapi_windows_vt100_support($stream)) - || false !== getenv('ANSICON') - || 'ON' === getenv('ConEmuANSI') - || 'xterm' === getenv('TERM'); + if ('dumb' === $term = (string) getenv('TERM')) { + return false; } - return stream_isatty($stream); + // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 + return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); } /** diff --git a/Tests/Caster/DoctrineCasterTest.php b/Tests/Caster/DoctrineCasterTest.php index 992c6c54..b0b0c90c 100644 --- a/Tests/Caster/DoctrineCasterTest.php +++ b/Tests/Caster/DoctrineCasterTest.php @@ -36,9 +36,9 @@ public function testCastPersistentCollection() $expected = << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests\Cloner; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\Cloner\Stub; + +final class StubTest extends TestCase +{ + public function testUnserializeNullValue() + { + $stub = new Stub(); + $stub->value = null; + + $stub = unserialize(serialize($stub)); + + self::assertNull($stub->value); + } + + public function testUnserializeNullInTypedProperty() + { + $stub = new MyStub(); + $stub->myProp = null; + + $stub = unserialize(serialize($stub)); + + self::assertNull($stub->myProp); + } + + public function testUninitializedStubPropertiesAreLeftUninitialized() + { + $stub = new MyStub(); + + $stub = unserialize(serialize($stub)); + + $r = new \ReflectionProperty(MyStub::class, 'myProp'); + self::assertFalse($r->isInitialized($stub)); + } +} + +final class MyStub extends Stub +{ + public mixed $myProp; +} diff --git a/Tests/Dumper/CliDumperTest.php b/Tests/Dumper/CliDumperTest.php index 37b70800..1e0e6bda 100644 --- a/Tests/Dumper/CliDumperTest.php +++ b/Tests/Dumper/CliDumperTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\VarDumper\Tests\Dumper; use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; +use Symfony\Component\VarDumper\Caster\ClassStub; use Symfony\Component\VarDumper\Caster\CutStub; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\Stub; @@ -346,9 +348,7 @@ public function testThrowingCaster() › twig source › } - %s%eTemplate.php:%d { …} - %s%eTemplate.php:%d { …} - %s%eTemplate.php:%d { …} + %A%eTemplate.php:%d { …} %s%eTests%eDumper%eCliDumperTest.php:%d { …} %A } } @@ -479,7 +479,7 @@ public function testCollapse() ], [ 'bar' => 123, - ] + ], ]); $dumper = new CliDumper(); @@ -499,4 +499,34 @@ public function testCollapse() $dump ); } + + public function testFileLinkFormat() + { + if (!class_exists(FileLinkFormatter::class)) { + $this->markTestSkipped(sprintf('Class "%s" is required to run this test.', FileLinkFormatter::class)); + } + + $data = new Data([ + [ + new ClassStub(self::class), + ], + ]); + + $ide = $_ENV['SYMFONY_IDE'] ?? null; + $_ENV['SYMFONY_IDE'] = 'vscode'; + + try { + $dumper = new CliDumper(); + $dumper->setColors(true); + $dump = $dumper->dump($data, true); + + $this->assertStringMatchesFormat('%svscode:%sCliDumperTest%s', $dump); + } finally { + if (null === $ide) { + unset($_ENV['SYMFONY_IDE']); + } else { + $_ENV['SYMFONY_IDE'] = $ide; + } + } + } }