nikic / PHP-Parser

A PHP parser written in PHP
BSD 3-Clause "New" or "Revised" License
17.07k stars 1.1k forks source link

Feature: PrettyPrint closures with either dereferencing or resolving of `self` usage #903

Closed boesing closed 1 year ago

boesing commented 1 year ago

Hey there,

I do have the following example which is broadly used in the mezzio (laminas) ecosystem as config-cache is passed to the filesystem while configuration is "provided" by one or multiple ConfigProvider instances .

Given the following example:

<?php

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\FindingVisitor;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter\Standard;

namespace Baz {
    class FooBar
    {
        private const CONSTANT = 'foo';

        public function getClosure(): Closure
        {
            return function (array $config): string {
                return $config[self::CONSTANT];
            };
        }
    };
}

namespace {

    use Baz\FooBar;
    use PhpParser\Node;
    use PhpParser\NodeTraverser;
    use PhpParser\NodeVisitor\FindingVisitor;
    use PhpParser\NodeVisitor\NameResolver;
    use PhpParser\ParserFactory;
    use PhpParser\PrettyPrinter\Standard;

    require __DIR__ . '/vendor/autoload.php';

    $object = new FooBar();
    $closure = $object->getClosure();

    $reflectionFunction = new ReflectionFunction($closure);

    $file = $reflectionFunction->getFileName();
    $line = $reflectionFunction->getStartLine();

    $source = file_get_contents(__FILE__);
    $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
    $ast = $parser->parse($source);

    $nodeTraverser = new NodeTraverser();
    $nodeTraverser->addVisitor(new NameResolver());

    $ast = $nodeTraverser->traverse($ast);

    $nodeFinder = new FindingVisitor(fn (Node $node) => ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction)
        && $node->getStartLine() === $line);

    $traverser = new NodeTraverser();
    $traverser->addVisitor($nodeFinder);
    $traverser->traverse($ast);

    $closureNode = $nodeFinder->getFoundNodes()[0];

    $prettyPrint = new Standard();

    var_dump( $prettyPrint->prettyPrintExpr($closureNode) );
}

Current output

string(73) "function (array $config) : string {
    return $config[self::CONSTANT];
}"

Preferred output

I'd love to get either the constant value dereferenced, i.e. 'foo' or using the FQCN instead of self.

string(73) "function (array $config) : string {
    return $config['foo']; // option 1
    return $config[\Baz\FooBar::CONSTANT]; // option 2
}"

Is there already a way to achieve this or would this be a whole new feature? Do you think that this would make sense in this library or does implementing such a feature is quite complex?

nikic commented 1 year ago

You should be able to easily implement this with a visitor -- remember the name of the class currently visited, and then replace references to self. However, note that this is not a behavior-preserving transform due to closure rebinding. self in closures is subject to rebinding.

In any case, I don't think this needs any particular additional support from the side of PHP-Parser.