feature #30301 [VarDumper] add link to source next to class names (nicolas-grekas)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[VarDumper] add link to source next to class names

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR adds a `^` next to language identifiers (class and callback names) both on the Web and on the CLI. Clicking it opens the IDE to the target source code:

Eg in the profiler:
![image](https://user-images.githubusercontent.com/243674/53021031-900c4380-3458-11e9-9439-260ff11f0910.png)

And in the CLI:
![image](https://user-images.githubusercontent.com/243674/53021092-b16d2f80-3458-11e9-9f03-cdab59da4585.png)

Commits
-------

5fcd6b1d4e [VarDumper] add link to source next to class names
This commit is contained in:
Fabien Potencier 2019-02-21 10:23:57 +01:00
commit cbe8cff882
12 changed files with 76 additions and 40 deletions

View File

@ -43,6 +43,11 @@ class DebugExtension extends Extension
->addMethodCall('setMinDepth', [$config['min_depth']])
->addMethodCall('setMaxString', [$config['max_string_length']]);
if (method_exists(ReflectionClass::class, 'unsetClosureFileInfo')) {
$container->getDefinition('var_dumper.cloner')
->addMethodCall('addCasters', ReflectionClass::UNSET_CLOSURE_FILE_INFO);
}
if (method_exists(HtmlDumper::class, 'setTheme') && 'dark' !== $config['theme']) {
$container->getDefinition('var_dumper.html_dumper')
->addMethodCall('setTheme', [$config['theme']]);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\VarDumper\Caster\CutStub;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
@ -79,7 +80,7 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable
*/
protected function getCasters()
{
return [
$casters = [
'*' => function ($v, array $a, Stub $s, $isNested) {
if (!$v instanceof Stub) {
foreach ($a as $k => $v) {
@ -92,5 +93,11 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable
return $a;
},
];
if (method_exists(ReflectionCaster::class, 'unsetClosureFileInfo')) {
$casters += ReflectionCaster::UNSET_CLOSURE_FILE_INFO;
}
return $casters;
}
}

View File

@ -87,21 +87,17 @@ class FileLinkFormatter
private function getFileLinkFormat()
{
if ($this->fileLinkFormat) {
return $this->fileLinkFormat;
}
if ($this->requestStack && $this->baseDir && $this->urlFormat) {
$request = $this->requestStack->getMasterRequest();
if ($request instanceof Request) {
if ($this->urlFormat instanceof \Closure && !$this->urlFormat = ($this->urlFormat)()) {
return;
}
return [
if ($request instanceof Request && (!$this->urlFormat instanceof \Closure || $this->urlFormat = ($this->urlFormat)())) {
$this->fileLinkFormat = [
$request->getSchemeAndHttpHost().$request->getBasePath().$this->urlFormat,
$this->baseDir.\DIRECTORY_SEPARATOR, '',
];
}
}
return $this->fileLinkFormat;
}
}

View File

@ -34,18 +34,6 @@ class FileLinkFormatterTest extends TestCase
$this->assertSame("debug://open?url=file://$file&line=3", $sut->format($file, 3));
}
public function testWhenFileLinkFormatAndRequest()
{
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';
$requestStack = new RequestStack();
$request = new Request();
$requestStack->push($request);
$sut = new FileLinkFormatter('debug://open?url=file://%f&line=%l', $requestStack, __DIR__, '/_profiler/open?file=%f&line=%l#line%l');
$this->assertSame("debug://open?url=file://$file&line=3", $sut->format($file, 3));
}
public function testWhenNoFileLinkFormatAndRequest()
{
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';

View File

@ -20,6 +20,8 @@ use Symfony\Component\VarDumper\Cloner\Stub;
*/
class ReflectionCaster
{
const UNSET_CLOSURE_FILE_INFO = ['Closure' => __CLASS__.'::unsetClosureFileInfo'];
private static $extraMap = [
'docComment' => 'getDocComment',
'extension' => 'getExtensionName',
@ -46,15 +48,20 @@ class ReflectionCaster
$stub->class .= self::getSignature($a);
if ($f = $c->getFileName()) {
$stub->attr['file'] = $f;
$stub->attr['line'] = $c->getStartLine();
}
unset($a[$prefix.'parameters']);
if ($filter & Caster::EXCLUDE_VERBOSE) {
$stub->cut += ($c->getFileName() ? 2 : 0) + \count($a);
return [];
}
unset($a[$prefix.'parameters']);
if ($f = $c->getFileName()) {
if ($f) {
$a[$prefix.'file'] = new LinkStub($f, $c->getStartLine());
$a[$prefix.'line'] = $c->getStartLine().' to '.$c->getEndLine();
}
@ -62,6 +69,13 @@ class ReflectionCaster
return $a;
}
public static function unsetClosureFileInfo(\Closure $c, array $a)
{
unset($a[Caster::PREFIX_VIRTUAL.'file'], $a[Caster::PREFIX_VIRTUAL.'line']);
return $a;
}
public static function castGenerator(\Generator $c, array $a, Stub $stub, $isNested)
{
if (!class_exists('ReflectionGenerator', false)) {

View File

@ -279,7 +279,7 @@ abstract class AbstractCloner implements ClonerInterface
$stub->class = get_parent_class($class).'@anonymous';
}
if (isset($this->classInfo[$class])) {
list($i, $parents, $hasDebugInfo) = $this->classInfo[$class];
list($i, $parents, $hasDebugInfo, $fileInfo) = $this->classInfo[$class];
} else {
$i = 2;
$parents = [$class];
@ -295,9 +295,16 @@ abstract class AbstractCloner implements ClonerInterface
}
$parents[] = '*';
$this->classInfo[$class] = [$i, $parents, $hasDebugInfo];
$r = new \ReflectionClass($class);
$fileInfo = $r->isInternal() || $r->isSubclassOf(Stub::class) ? [] : [
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
$this->classInfo[$class] = [$i, $parents, $hasDebugInfo, $fileInfo];
}
$stub->attr += $fileInfo;
$a = Caster::castObject($obj, $class, $hasDebugInfo);
try {

View File

@ -274,6 +274,7 @@ class CliDumper extends AbstractDumper
public function enterHash(Cursor $cursor, $type, $class, $hasChild)
{
$this->dumpKey($cursor);
$attr = $cursor->attr;
if ($this->collapseNextHash) {
$cursor->skipChildren = true;
@ -282,11 +283,11 @@ class CliDumper extends AbstractDumper
$class = $this->utf8Encode($class);
if (Cursor::HASH_OBJECT === $type) {
$prefix = $class && 'stdClass' !== $class ? $this->style('note', $class).' {' : '{';
$prefix = $class && 'stdClass' !== $class ? $this->style('note', $class, $attr).' {' : '{';
} elseif (Cursor::HASH_RESOURCE === $type) {
$prefix = $this->style('note', $class.' resource').($hasChild ? ' {' : ' ');
$prefix = $this->style('note', $class.' resource', $attr).($hasChild ? ' {' : ' ');
} else {
$prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class).' [' : '[';
$prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class, $attr).' [' : '[';
}
if ($cursor->softRefCount || 0 < $cursor->softRefHandle) {
@ -454,11 +455,9 @@ class CliDumper extends AbstractDumper
goto href;
}
$style = $this->styles[$style];
$map = static::$controlCharsMap;
$startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : '';
$endCchr = $this->colors ? "\033[m\033[{$style}m" : '';
$endCchr = $this->colors ? "\033[m\033[{$this->styles[$style]}m" : '';
$value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) {
$s = $startCchr;
$c = $c[$i = 0];
@ -473,7 +472,7 @@ class CliDumper extends AbstractDumper
if ($cchrCount && "\033" === $value[0]) {
$value = substr($value, \strlen($startCchr));
} else {
$value = "\033[{$style}m".$value;
$value = "\033[{$this->styles[$style]}m".$value;
}
if ($cchrCount && $endCchr === substr($value, -\strlen($endCchr))) {
$value = substr($value, 0, -\strlen($endCchr));
@ -485,7 +484,11 @@ class CliDumper extends AbstractDumper
href:
if ($this->colors && $this->handlesHrefGracefully) {
if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) {
$attr['href'] = $href;
if ('note' === $style) {
$value .= "\033]8;;{$href}\033\\^\033]8;;\033\\";
} else {
$attr['href'] = $href;
}
}
if (isset($attr['href'])) {
$value = "\033]8;;{$attr['href']}\033\\{$value}\033]8;;\033\\";
@ -632,7 +635,7 @@ class CliDumper extends AbstractDumper
private function getSourceLink($file, $line)
{
if ($fmt = $this->displayOptions['fileLinkFormat']) {
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : ($fmt->format($file, $line) ?: 'file://'.$file);
}
return false;

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\VarDumper\Dumper\ContextProvider;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
/**
@ -29,6 +30,7 @@ final class RequestContextProvider implements ContextProviderInterface
$this->requestStack = $requestStack;
$this->cloner = new VarCloner();
$this->cloner->setMaxItems(0);
$this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
}
public function getContext(): ?array

View File

@ -314,13 +314,17 @@ return function (root, x) {
}
function a(e, f) {
addEventListener(root, e, function (e) {
addEventListener(root, e, function (e, n) {
if ('A' == e.target.tagName) {
f(e.target, e);
} else if ('A' == e.target.parentNode.tagName) {
f(e.target.parentNode, e);
} else if (e.target.nextElementSibling && 'A' == e.target.nextElementSibling.tagName) {
f(e.target.nextElementSibling, e, true);
} else if ((n = e.target.nextElementSibling) && 'A' == n.tagName) {
if (!/\bsf-dump-toggle\b/.test(n.className)) {
n = n.nextElementSibling;
}
f(n, e, true);
}
});
};
@ -852,7 +856,13 @@ EOHTML
} elseif ('str' === $style && 1 < $attr['length']) {
$style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : '');
} elseif ('note' === $style && false !== $c = strrpos($v, '\\')) {
return sprintf('<abbr title="%s" class=sf-dump-%s>%s</abbr>', $v, $style, substr($v, $c + 1));
if (isset($attr['file']) && $link = $this->getSourceLink($attr['file'], isset($attr['line']) ? $attr['line'] : 0)) {
$link = sprintf('<a href="%s" rel="noopener noreferrer">^</a>', esc($this->utf8Encode($link)));
} else {
$link = '';
}
return sprintf('<abbr title="%s" class=sf-dump-%s>%s</abbr>%s', $v, $style, substr($v, $c + 1), $link);
} elseif ('protected' === $style) {
$style .= ' title="Protected property"';
} elseif ('meta' === $style && isset($attr['title'])) {

View File

@ -114,7 +114,7 @@ EOTXT
{
$var = function &($a = 5) {};
$this->assertDumpEquals('Closure&($a = 5) { …6}', $var, Caster::EXCLUDE_VERBOSE);
$this->assertDumpEquals('Closure&($a = 5) { …5}', $var, Caster::EXCLUDE_VERBOSE);
}
public function testReflectionParameter()

View File

@ -411,6 +411,8 @@ Symfony\Component\VarDumper\Cloner\Data Object
[position] => 1
[attr] => Array
(
[file] => %a%eVarClonerTest.php
[line] => 20
)
)

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\VarDumper;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
@ -29,6 +30,7 @@ class VarDumper
{
if (null === self::$handler) {
$cloner = new VarCloner();
$cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
if (isset($_SERVER['VAR_DUMPER_FORMAT'])) {
$dumper = 'html' === $_SERVER['VAR_DUMPER_FORMAT'] ? new HtmlDumper() : new CliDumper();