From 2937ffa12510dcccff5b4a59d689ff546e0d578c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 31 Aug 2016 10:39:24 +0200 Subject: [PATCH] [VarDumper] Handle attributes in Data clones for more semantic dumps --- .../views/Profiler/profiler.css.twig | 5 ++ .../VarDumper/Caster/ExceptionCaster.php | 50 +++++++++------ .../Component/VarDumper/Caster/StubCaster.php | 2 + .../Component/VarDumper/Cloner/Cursor.php | 1 + .../Component/VarDumper/Cloner/Data.php | 3 + .../Component/VarDumper/Cloner/Stub.php | 1 + .../Component/VarDumper/Dumper/CliDumper.php | 10 +-- .../Component/VarDumper/Dumper/HtmlDumper.php | 63 +++++++++++++++---- .../Tests/Caster/ExceptionCasterTest.php | 38 +++++------ .../Tests/Caster/ReflectionCasterTest.php | 30 ++++----- .../VarDumper/Tests/CliDumperTest.php | 30 ++++----- .../VarDumper/Tests/VarClonerTest.php | 30 ++++++++- 12 files changed, 177 insertions(+), 86 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 62250ba8f320f..697be27ada9e7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -912,6 +912,11 @@ table.logs .sf-call-stack abbr { #collector-content .sf-dump .trace li.selected { background: rgba(255, 255, 153, 0.5); } +#collector-content .sf-dump-expanded code { color: #222; } +#collector-content .sf-dump-expanded code .sf-dump-const { + background: rgba(255, 255, 153, 0.5); + font-weight: normal; +} {# Search Results page ========================================================================= #} diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php index 70bda0b243ff2..4765fbf6db907 100644 --- a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php @@ -89,7 +89,6 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is $stub->handle = 0; $frames = $trace->value; $prefix = Caster::PREFIX_VIRTUAL; - $format = "\0~Stack level %s.\0%s"; $a = array(); $j = count($frames); @@ -99,14 +98,14 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is if (!isset($trace->value[$i])) { return array(); } - $lastCall = isset($frames[$i]['function']) ? ' ==> '.(isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : ''; + $lastCall = isset($frames[$i]['function']) ? (isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : ''; $frames[] = array('function' => ''); for ($j += $trace->numberingOffset - $i++; isset($frames[$i]); ++$i, --$j) { $f = $frames[$i]; $call = isset($f['function']) ? (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'].'()' : '???'; - $label = $call.$lastCall; + $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$lastCall; $frame = new FrameStub( array( 'object' => isset($f['object']) ? $f['object'] : null, @@ -120,14 +119,15 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $is $f = self::castFrameStub($frame, array(), $frame, true); if (isset($f[$prefix.'src'])) { foreach ($f[$prefix.'src']->value as $label => $frame) { + $label = substr_replace($label, "title=Stack level $j.&", 2, 0); } if (isset($f[$prefix.'args']) && $frame instanceof EnumStub) { $frame->value['args'] = $f[$prefix.'args']; } } - $a[sprintf($format, $j, $label)] = $frame; + $a[$label] = $frame; - $lastCall = ' ==> '.$call; + $lastCall = $call; } if (null !== $trace->sliceLength) { $a = array_slice($a, 0, $trace->sliceLength, true); @@ -149,28 +149,38 @@ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $is $f['file'] = substr($f['file'], 0, -strlen($match[0])); $f['line'] = (int) $match[1]; } - $src = array(); + $caller = isset($f['function']) ? sprintf('in %s() on line %d', (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'], $f['line']) : null; + $src = $f['line']; + $srcKey = $f['file']; + $ellipsis = explode(DIRECTORY_SEPARATOR, $srcKey); + $ellipsis = 3 < count($ellipsis) ? 2 + strlen(implode(array_slice($ellipsis, -2))) : 0; + if (file_exists($f['file']) && 0 <= self::$srcContext) { if (!empty($f['class']) && is_subclass_of($f['class'], 'Twig_Template') && method_exists($f['class'], 'getDebugInfo')) { $template = isset($f['object']) ? $f['object'] : new $f['class'](new \Twig_Environment(new \Twig_Loader_Filesystem())); try { + $ellipsis = 0; $templateName = $template->getTemplateName(); $templateSrc = explode("\n", method_exists($template, 'getSource') ? $template->getSource() : $template->getEnvironment()->getLoader()->getSource($templateName)); $templateInfo = $template->getDebugInfo(); if (isset($templateInfo[$f['line']])) { - $src[$templateName.':'.$templateInfo[$f['line']]] = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext); + $src = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext, $caller, 'twig'); + $srcKey = $templateName.':'.$templateInfo[$f['line']]; } } catch (\Twig_Error_Loader $e) { } } - if (!$src) { - $src[$f['file'].':'.$f['line']] = self::extractSource(explode("\n", file_get_contents($f['file'])), $f['line'], self::$srcContext); + if ($srcKey == $f['file']) { + $src = self::extractSource(explode("\n", file_get_contents($f['file'])), $f['line'], self::$srcContext, $caller, 'php', $f['file']); + $srcKey .= ':'.$f['line']; + if ($ellipsis) { + $ellipsis += 1 + strlen($f['line']); + } } - } else { - $src[$f['file']] = $f['line']; } - $a[$prefix.'src'] = new EnumStub($src); + $srcAttr = $ellipsis ? 'ellipsis='.$ellipsis : ''; + $a[$prefix.'src'] = new EnumStub(array("\0~$srcAttr\0$srcKey" => $src)); } unset($a[$prefix.'args'], $a[$prefix.'line'], $a[$prefix.'file']); @@ -214,7 +224,7 @@ private static function filterExceptionArray($xClass, array $a, $xPrefix, $filte return $a; } - private static function extractSource(array $srcArray, $line, $srcContext) + private static function extractSource(array $srcArray, $line, $srcContext, $title, $lang, $file = null) { $src = array(); @@ -239,8 +249,6 @@ private static function extractSource(array $srcArray, $line, $srcContext) } while (0 > $i && null !== $pad); --$ltrim; - - $pad = strlen($line + $srcContext); $srcArray = array(); foreach ($src as $i => $c) { @@ -248,11 +256,17 @@ private static function extractSource(array $srcArray, $line, $srcContext) $c = isset($c[$ltrim]) && "\r" !== $c[$ltrim] ? substr($c, $ltrim) : ltrim($c, " \t"); } $c = substr($c, 0, -1); - $c = new ConstStub($c, $c); if ($i !== $srcContext) { - $c->class = 'default'; + $c = new ConstStub('default', $c); + } else { + $c = new ConstStub($c, $title); + if (null !== $file) { + $c->attr['file'] = $file; + $c->attr['line'] = $line; + } } - $srcArray[sprintf("% {$pad}d", $i + $line - $srcContext)] = $c; + $c->attr['lang'] = $lang; + $srcArray[sprintf("\0~%d\0", $i + $line - $srcContext)] = $c; } return new EnumStub($srcArray); diff --git a/src/Symfony/Component/VarDumper/Caster/StubCaster.php b/src/Symfony/Component/VarDumper/Caster/StubCaster.php index f76200f5007c7..62a79fd1d75d5 100644 --- a/src/Symfony/Component/VarDumper/Caster/StubCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/StubCaster.php @@ -28,6 +28,7 @@ public static function castStub(Stub $c, array $a, Stub $stub, $isNested) $stub->value = $c->value; $stub->handle = $c->handle; $stub->cut = $c->cut; + $stub->attr = $c->attr; return array(); } @@ -56,6 +57,7 @@ public static function castEnum(EnumStub $c, array $a, Stub $stub, $isNested) $stub->handle = 0; $stub->value = null; $stub->cut = $c->cut; + $stub->attr = $c->attr; $a = array(); diff --git a/src/Symfony/Component/VarDumper/Cloner/Cursor.php b/src/Symfony/Component/VarDumper/Cloner/Cursor.php index 162f3293cf226..9db55992e4aa9 100644 --- a/src/Symfony/Component/VarDumper/Cloner/Cursor.php +++ b/src/Symfony/Component/VarDumper/Cloner/Cursor.php @@ -38,4 +38,5 @@ class Cursor public $hashLength = 0; public $hashCut = 0; public $stop = false; + public $attr = array(); } diff --git a/src/Symfony/Component/VarDumper/Cloner/Data.php b/src/Symfony/Component/VarDumper/Cloner/Data.php index 3434a2430071b..1c6dd0df86b60 100644 --- a/src/Symfony/Component/VarDumper/Cloner/Data.php +++ b/src/Symfony/Component/VarDumper/Cloner/Data.php @@ -155,6 +155,7 @@ private function dumpItem($dumper, $cursor, &$refs, $item) $firstSeen = true; if (!$item instanceof Stub) { + $cursor->attr = array(); $type = gettype($item); } elseif (Stub::TYPE_REF === $item->type) { if ($item->handle) { @@ -167,6 +168,7 @@ private function dumpItem($dumper, $cursor, &$refs, $item) $cursor->hardRefHandle = $this->useRefHandles & $item->handle; $cursor->hardRefCount = $item->refCount; } + $cursor->attr = $item->attr; $type = $item->class ?: gettype($item->value); $item = $item->value; } @@ -181,6 +183,7 @@ private function dumpItem($dumper, $cursor, &$refs, $item) } $cursor->softRefHandle = $this->useRefHandles & $item->handle; $cursor->softRefCount = $item->refCount; + $cursor->attr = $item->attr; $cut = $item->cut; if ($item->position && $firstSeen) { diff --git a/src/Symfony/Component/VarDumper/Cloner/Stub.php b/src/Symfony/Component/VarDumper/Cloner/Stub.php index f58a57a72761b..313c591fc835a 100644 --- a/src/Symfony/Component/VarDumper/Cloner/Stub.php +++ b/src/Symfony/Component/VarDumper/Cloner/Stub.php @@ -37,4 +37,5 @@ class Stub public $handle = 0; public $refCount = 0; public $position = 0; + public $attr = array(); } diff --git a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php index 4cfa748799daf..ce44a550cbd58 100644 --- a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php @@ -112,7 +112,7 @@ public function dumpScalar(Cursor $cursor, $type, $value) $this->dumpKey($cursor); $style = 'const'; - $attr = array(); + $attr = $cursor->attr; switch ($type) { case 'default': @@ -148,7 +148,7 @@ public function dumpScalar(Cursor $cursor, $type, $value) break; default: - $attr['value'] = isset($value[0]) && !preg_match('//u', $value) ? $this->utf8Encode($value) : $value; + $attr += array('value' => isset($value[0]) && !preg_match('//u', $value) ? $this->utf8Encode($value) : $value); $value = isset($type[0]) && !preg_match('//u', $type) ? $this->utf8Encode($type) : $type; break; } @@ -164,6 +164,7 @@ public function dumpScalar(Cursor $cursor, $type, $value) public function dumpString(Cursor $cursor, $str, $bin, $cut) { $this->dumpKey($cursor); + $attr = $cursor->attr; if ($bin) { $str = $this->utf8Encode($str); @@ -172,7 +173,7 @@ public function dumpString(Cursor $cursor, $str, $bin, $cut) $this->line .= '""'; $this->dumpLine($cursor->depth, true); } else { - $attr = array( + $attr += array( 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0, 'binary' => $bin, ); @@ -350,7 +351,8 @@ protected function dumpKey(Cursor $cursor) case '~': $style = 'meta'; if (isset($key[0][1])) { - $attr['title'] = substr($key[0], 1); + parse_str(substr($key[0], 1), $attr); + $attr += array('binary' => $cursor->hashKeyIsBinary); } break; case '*': diff --git a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php index 95b9c04175b20..b01852e1c03d1 100644 --- a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php @@ -43,11 +43,13 @@ class HtmlDumper extends CliDumper 'meta' => 'color:#B729D9', 'key' => 'color:#56DB3A', 'index' => 'color:#1299DA', + 'expanded code.hljs' => 'display:inline; padding:0; background:none', ); private $displayOptions = array( 'maxDepth' => 1, 'maxStringLength' => 160, + 'fileLinkFormat' => null, ); private $extraDisplayOptions = array(); @@ -184,6 +186,19 @@ function toggle(a, recursive) { return function (root, x) { root = doc.getElementById(root); + var indentRx = new RegExp('^('+(root.getAttribute('data-indent-pad') || ' ').replace(rxEsc, '\\$1')+')+', 'm'), + options = {$options}, + elt = root.getElementsByTagName('A'), + len = elt.length, + i = 0, s, h, + t = []; + + while (i < len) t.push(elt[i++]); + + for (i in x) { + options[i] = x[i]; + } + function a(e, f) { addEventListener(root, e, function (e) { if ('A' == e.target.tagName) { @@ -201,6 +216,20 @@ function isCtrlKey(e) { refStyle.innerHTML = ''; } }); + if (options.fileLinkFormat) { + addEventListener(root, 'click', function (e) { + e = e.target; + while (root != e && 'CODE' != e.tagName) { + e = e.parentNode; + } + if ('CODE' == e.tagName) { + var f = e.getAttribute('data-file'), l = e.getAttribute('data-line'); + if (f && l) { + location.href = options.fileLinkFormat.replace('%f', f).replace('%l', l); + } + } + }); + } a('mouseover', function (a) { if (a = idRx.exec(a.className)) { try { @@ -246,19 +275,6 @@ function isCtrlKey(e) { } }); - var indentRx = new RegExp('^('+(root.getAttribute('data-indent-pad') || ' ').replace(rxEsc, '\\$1')+')+', 'm'), - options = {$options}, - elt = root.getElementsByTagName('A'), - len = elt.length, - i = 0, s, h, - t = []; - - while (i < len) t.push(elt[i++]); - - for (i in x) { - options[i] = x[i]; - } - elt = root.getElementsByTagName('SAMP'); len = elt.length; i = 0; @@ -369,6 +385,15 @@ function isCtrlKey(e) { border: 0; outline: none; } +pre.sf-dump .sf-dump-ellipsis { + display: inline-block; + overflow: visible; + text-overflow: ellipsis; + width: 50px; + white-space: nowrap; + overflow: hidden; + vertical-align: top; +} .sf-dump-str-collapse .sf-dump-str-collapse { display: none; } @@ -452,6 +477,11 @@ protected function style($style, $value, $attr = array()) } elseif ('private' === $style) { $style .= sprintf(' title="Private property defined in class: `%s`"', esc($attr['class'])); } + if (isset($attr['ellipsis'])) { + $label = esc(substr($value, -$attr['ellipsis'])); + + return sprintf('%2$s%s', $style, substr($v, 0, -strlen($label)), $label); + } $map = static::$controlCharsMap; $style = ""; @@ -475,6 +505,13 @@ protected function style($style, $value, $attr = array()) } else { $v .= ''; } + if (isset($attr['lang'])) { + if (isset($attr['file'], $attr['line'])) { + $v = sprintf('%s', esc($attr['lang']), esc($attr['file']), $attr['line'], $v); + } else { + $v = sprintf('%s', esc($attr['lang']), $v); + } + } return $v; } diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php index 71ecdb79f4c6a..72f2fa8223a6e 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ExceptionCasterTest.php @@ -43,14 +43,14 @@ public function testDefaultSettings() #line: 25 -trace: { %sExceptionCasterTest.php:25: { - 24: { - 25: return new \Exception('foo'); - 26: } + : { + : return new \Exception('foo'); + : } } %sExceptionCasterTest.php:%d: { - %d: { - %d: $e = $this->getTestException(1); - %d: + : { + : $e = $this->getTestException(1); + : args: { 1 } @@ -68,14 +68,14 @@ public function testSeek() $expectedDump = <<<'EODUMP' { %sExceptionCasterTest.php:25: { - 24: { - 25: return new \Exception('foo'); - 26: } + : { + : return new \Exception('foo'); + : } } %sExceptionCasterTest.php:%d: { - %d: { - %d: $e = $this->getTestException(2); - %d: + : { + : $e = $this->getTestException(2); + : args: { 2 } @@ -99,14 +99,14 @@ public function testNoArgs() #line: 25 -trace: { %sExceptionCasterTest.php:25: { - 24: { - 25: return new \Exception('foo'); - 26: } + : { + : return new \Exception('foo'); + : } } %sExceptionCasterTest.php:%d: { - %d: { - %d: $e = $this->getTestException(1); - %d: ExceptionCaster::$traceArgs = false; + : { + : $e = $this->getTestException(1); + : ExceptionCaster::$traceArgs = false; } %A EODUMP; @@ -156,7 +156,7 @@ public function testHtmlDump() #file: "%sExceptionCasterTest.php" #line: 25 -trace: { - %sExceptionCasterTest.php: 25 + %sVarDumper%eTests%eCaster%eExceptionCasterTest.php: 25 …12 } } diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 7cc90e9d19fed..f4cc50efc597c 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -157,9 +157,9 @@ public function testGenerator() executing: { Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz(): { %sGeneratorDemo.php:14: { - 13: { - 14: yield from bar(); - 15: } + : { + : yield from bar(); + : } } } } @@ -178,19 +178,19 @@ public function testGenerator() this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} trace: { %sGeneratorDemo.php:9: { - 8: { - 9: yield 1; - 10: } + : { + : yield 1; + : } } %sGeneratorDemo.php:20: { - 19: { - 20: yield from GeneratorDemo::foo(); - 21: } + : { + : yield from GeneratorDemo::foo(); + : } } %sGeneratorDemo.php:14: { - 13: { - 14: yield from bar(); - 15: } + : { + : yield from bar(); + : } } } } @@ -198,9 +198,9 @@ public function testGenerator() executing: { Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo(): { %sGeneratorDemo.php:10: { - 9: yield 1; - 10: } - 11: + : yield 1; + : } + : } } } diff --git a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php index e870d5b1ce3b4..ee261832cc04b 100644 --- a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php @@ -264,9 +264,9 @@ public function testThrowingCaster() if (method_exists($twig, 'getSource')) { $twig = <<doDisplay(\$context, \$blocks); - %d: } catch (Twig_Error \$e) { + : try { + : \$this->doDisplay(\$context, \$blocks); + : } catch (Twig_Error \$e) { } %sTemplate.php:%d: { - %d: { - %d: \$this->displayWithErrorHandling(\$this->env->mergeGlobals(\$context), array_merge(\$this->blocks, \$blocks)); - %d: } + : { + : \$this->displayWithErrorHandling(\$this->env->mergeGlobals(\$context), array_merge(\$this->blocks, \$blocks)); + : } } %sTemplate.php:%d: { - %d: try { - %d: \$this->display(\$context); - %d: } catch (Exception \$e) { + : try { + : \$this->display(\$context); + : } catch (Exception \$e) { } %sCliDumperTest.php:{$line}: { - %d: } - {$line}: };'), - %d: )); + : } + : };'), + : )); } } } diff --git a/src/Symfony/Component/VarDumper/Tests/VarClonerTest.php b/src/Symfony/Component/VarDumper/Tests/VarClonerTest.php index 83accce2d9dd7..eb2d0b0eda65c 100644 --- a/src/Symfony/Component/VarDumper/Tests/VarClonerTest.php +++ b/src/Symfony/Component/VarDumper/Tests/VarClonerTest.php @@ -41,6 +41,10 @@ public function testMaxIntBoundary() [handle] => 0 [refCount] => 0 [position] => 1 + [attr] => Array + ( + ) + ) ) @@ -86,6 +90,10 @@ public function testClone() [handle] => %i [refCount] => 0 [position] => 1 + [attr] => Array + ( + ) + ) ) @@ -101,6 +109,10 @@ public function testClone() [handle] => %i [refCount] => 0 [position] => 2 + [attr] => Array + ( + ) + ) [\000+\0002] => Symfony\Component\VarDumper\Cloner\Stub Object @@ -112,6 +124,10 @@ public function testClone() [handle] => %i [refCount] => 0 [position] => 3 + [attr] => Array + ( + ) + ) ) @@ -153,7 +169,7 @@ public function testJsonCast() [0]=> array(1) { [0]=> - object(Symfony\Component\VarDumper\Cloner\Stub)#%i (7) { + object(Symfony\Component\VarDumper\Cloner\Stub)#%i (8) { ["type"]=> string(5) "array" ["class"]=> @@ -168,12 +184,15 @@ public function testJsonCast() int(0) ["position"]=> int(1) + ["attr"]=> + array(0) { + } } } [1]=> array(1) { ["1"]=> - object(Symfony\Component\VarDumper\Cloner\Stub)#%i (7) { + object(Symfony\Component\VarDumper\Cloner\Stub)#%i (8) { ["type"]=> string(6) "object" ["class"]=> @@ -188,6 +207,9 @@ public function testJsonCast() int(0) ["position"]=> int(0) + ["attr"]=> + array(0) { + } } } } @@ -239,6 +261,10 @@ public function testCaster() [handle] => %i [refCount] => 0 [position] => 1 + [attr] => Array + ( + ) + ) )