diff --git a/fixtures/set024/main.php b/fixtures/set024/main.php index 3eeda854a..16adce6a2 100644 --- a/fixtures/set024/main.php +++ b/fixtures/set024/main.php @@ -4,6 +4,12 @@ namespace Acme; -require_once __DIR__ . '/vendor/autoload.php'; +use function file_exists; + +if (file_exists($autoload = __DIR__ . '/vendor/scoper-autoload.php')) { + require_once $autoload; +} else { + require_once __DIR__ . '/vendor/autoload.php'; +} dump('yo'); diff --git a/scoper.inc.php b/scoper.inc.php index f1673c713..547a49485 100644 --- a/scoper.inc.php +++ b/scoper.inc.php @@ -15,7 +15,6 @@ use Isolated\Symfony\Component\Finder\Finder; return [ - 'prefix' => 'Foo', 'whitelist' => [ Finder::class, ], diff --git a/src/Autoload/ScoperAutoloadGenerator.php b/src/Autoload/ScoperAutoloadGenerator.php index 315d35b9e..9774cabba 100644 --- a/src/Autoload/ScoperAutoloadGenerator.php +++ b/src/Autoload/ScoperAutoloadGenerator.php @@ -14,8 +14,13 @@ namespace Humbug\PhpScoper\Autoload; +use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UserGlobalFunctionCollection; use Humbug\PhpScoper\Whitelist; use function array_map; +use function iterator_to_array; +use const PHP_EOL; +use PhpParser\Node\Name\FullyQualified; +use function sprintf; final class ScoperAutoloadGenerator { @@ -28,9 +33,8 @@ public function __construct(Whitelist $whitelist) public function dump(string $prefix): string { - $statements = $this->createStatements($prefix); - - $statements = implode(PHP_EOL, $statements); + $statements = implode(PHP_EOL, $this->createClassAliasStatements($prefix)).PHP_EOL; + $statements .= implode(PHP_EOL, $this->createFunctionAliasStatements($this->whitelist->getUserGlobalFunctions())); return <<whitelist->getClassWhitelistArray() ); } + + /** + * @return string[] + */ + public function createFunctionAliasStatements(UserGlobalFunctionCollection $userGlobalFunctions): array + { + return array_map( + function (array $node): string { + /** + * @var FullyQualified $original + * @var FullyQualified $alias + */ + [$original, $alias] = $node; + + return sprintf( + <<<'PHP' +if (!function_exists('%1$s')) { + function %1$s() { + return \%2$s(func_get_args()); + } +} +PHP + , + $original->toString(), + $alias->toString() + ); + }, + iterator_to_array($userGlobalFunctions) + ); + } } diff --git a/src/PhpParser/NodeVisitor/Collection/NamespaceStmtCollection.php b/src/PhpParser/NodeVisitor/Collection/NamespaceStmtCollection.php index 8c0dd72ba..674c75734 100644 --- a/src/PhpParser/NodeVisitor/Collection/NamespaceStmtCollection.php +++ b/src/PhpParser/NodeVisitor/Collection/NamespaceStmtCollection.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\PhpParser\NodeVisitor\Collection; use ArrayIterator; +use function count; use Countable; use Humbug\PhpScoper\PhpParser\NodeVisitor\AppendParentNode; use IteratorAggregate; diff --git a/src/PhpParser/NodeVisitor/Collection/UserGlobalFunctionCollection.php b/src/PhpParser/NodeVisitor/Collection/UserGlobalFunctionCollection.php new file mode 100644 index 000000000..0a1bbb530 --- /dev/null +++ b/src/PhpParser/NodeVisitor/Collection/UserGlobalFunctionCollection.php @@ -0,0 +1,41 @@ +nodes[] = [$original, $alias]; + } + + /** + * @inheritdoc + */ + public function count(): int + { + return count($this->nodes); + } + + /** + * @inheritdoc + */ + public function getIterator(): iterable + { + return new ArrayIterator($this->nodes); + } +} \ No newline at end of file diff --git a/src/PhpParser/NodeVisitor/FunctionIdentifierRecorder.php b/src/PhpParser/NodeVisitor/FunctionIdentifierRecorder.php new file mode 100644 index 000000000..7434df010 --- /dev/null +++ b/src/PhpParser/NodeVisitor/FunctionIdentifierRecorder.php @@ -0,0 +1,90 @@ +, + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Humbug\PhpScoper\PhpParser\NodeVisitor; + +use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UserGlobalFunctionCollection; +use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\FullyQualifiedNameResolver; +use Humbug\PhpScoper\Whitelist; +use PhpParser\Node; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Stmt\Function_; +use PhpParser\NodeVisitorAbstract; +use function count; + +/** + * @TODO + * + * @private + */ +final class FunctionIdentifierRecorder extends NodeVisitorAbstract +{ + private $prefix; + private $nameResolver; + private $whitelist; + + public function __construct( + string $prefix, + FullyQualifiedNameResolver $nameResolver, + Whitelist $whitelist + ) { + $this->prefix = $prefix; + $this->nameResolver = $nameResolver; + $this->whitelist = $whitelist; + } + + /** + * @inheritdoc + */ + public function enterNode(Node $node): Node + { + if (false === ($node instanceof Identifier) || false === AppendParentNode::hasParent($node)) { + return $node; + } + + $parent = AppendParentNode::getParent($node); + + if (false === ($parent instanceof Function_)) { + return $node; + } + + /** @var Identifier $node */ + + $resolvedValue = $this->nameResolver->resolveName( + new Name( + $node->name, + $node->getAttributes() + ) + ); + $resolvedName = $resolvedValue->getName(); + + if (null !== $resolvedValue->getNamespace() + || false === ($resolvedName instanceof FullyQualified) + || count($resolvedName->parts) > 1 + ) { + return $node; + } + + /** @var FullyQualified $resolvedName */ + + $this->whitelist->recordUserGlobalFunction( + $resolvedName, + FullyQualified::concat($this->prefix, $resolvedName) + ); + + return $node; + } +} diff --git a/src/PhpParser/NodeVisitor/StringScalarPrefixer.php b/src/PhpParser/NodeVisitor/StringScalarPrefixer.php index 92a3e0151..dd14084c3 100644 --- a/src/PhpParser/NodeVisitor/StringScalarPrefixer.php +++ b/src/PhpParser/NodeVisitor/StringScalarPrefixer.php @@ -93,7 +93,7 @@ public function enterNode(Node $node): Node private function shouldPrefixScalar(Node $node, bool &$isSpecialFunction): bool { if (false === ($node instanceof String_ && AppendParentNode::hasParent($node) && is_string($node->value)) - || 1 !== preg_match('/^((\\\\)?[\p{L}_]+)|((\\\\)?(?:[\p{L}_]+\\\\+)+[\p{L}_]+)$/u', $node->value) + || 1 !== preg_match('/^((\\\\)?[\p{L}_]+)$|((\\\\)?(?:[\p{L}_]+\\\\+)+[\p{L}_]+)$/u', $node->value) ) { return false; } diff --git a/src/PhpParser/TraverserFactory.php b/src/PhpParser/TraverserFactory.php index d9002614a..8b929deb8 100644 --- a/src/PhpParser/TraverserFactory.php +++ b/src/PhpParser/TraverserFactory.php @@ -14,6 +14,7 @@ namespace Humbug\PhpScoper\PhpParser; +use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UserGlobalFunctionCollection; use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\NamespaceStmtCollection; use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UseStmtCollection; use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\FullyQualifiedNameResolver; @@ -49,6 +50,7 @@ public function create(string $prefix, Whitelist $whitelist): NodeTraverserInter $traverser->addVisitor(new NodeVisitor\UseStmt\UseStmtCollector($namespaceStatements, $useStatements)); $traverser->addVisitor(new NodeVisitor\UseStmt\UseStmtPrefixer($prefix, $whitelist, $this->reflector)); + $traverser->addVisitor(new NodeVisitor\FunctionIdentifierRecorder($prefix, $nameResolver, $whitelist)); $traverser->addVisitor(new NodeVisitor\NameStmtPrefixer($prefix, $whitelist, $nameResolver, $this->reflector)); $traverser->addVisitor(new NodeVisitor\StringScalarPrefixer($prefix, $whitelist, $this->reflector)); diff --git a/src/Whitelist.php b/src/Whitelist.php index 4bf65c24e..b280b5775 100644 --- a/src/Whitelist.php +++ b/src/Whitelist.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper; use Countable; +use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UserGlobalFunctionCollection; use InvalidArgumentException; use function array_filter; use function array_map; @@ -24,6 +25,7 @@ use function explode; use function implode; use function in_array; +use PhpParser\Node\Name\FullyQualified; use function sprintf; use function strtolower; use function substr; @@ -36,6 +38,8 @@ final class Whitelist implements Countable private $constants; private $namespaces; private $whitelistGlobalConstants; + private $whitelistGlobalFunctions; + private $userGlobalFunctions; public static function create(bool $whitelistGlobalConstants, bool $whitelistGlobalFunctions,string ...$elements): self { @@ -72,6 +76,7 @@ public static function create(bool $whitelistGlobalConstants, bool $whitelistGlo return new self( $whitelistGlobalConstants, + $whitelistGlobalFunctions, array_unique($original), array_unique($classes), array_unique($constants), @@ -87,16 +92,29 @@ public static function create(bool $whitelistGlobalConstants, bool $whitelistGlo */ private function __construct( bool $whitelistGlobalConstants, + bool $whitelistGlobalFunctions, array $original, array $classes, array $constants, array $namespaces ) { $this->whitelistGlobalConstants = $whitelistGlobalConstants; + $this->whitelistGlobalFunctions = $whitelistGlobalFunctions; $this->original = $original; $this->classes = $classes; $this->constants = $constants; $this->namespaces = $namespaces; + $this->userGlobalFunctions = new UserGlobalFunctionCollection(); + } + + public function recordUserGlobalFunction(FullyQualified $original, FullyQualified $alias): void + { + $this->userGlobalFunctions->add($original, $alias); + } + + public function getUserGlobalFunctions(): UserGlobalFunctionCollection + { + return $this->userGlobalFunctions; } public function whitelistGlobalConstants(): bool @@ -104,6 +122,12 @@ public function whitelistGlobalConstants(): bool return $this->whitelistGlobalConstants; } + public function whitelistGlobalFunctions(): bool + { + // TODO: check that nothing is appended/collected if everything is being whitelisted; avoid the collection in this case to avoid performance issues + return $this->whitelistGlobalFunctions; + } + public function isClassWhitelisted(string $name): bool { return in_array(strtolower($name), $this->classes, true); diff --git a/tests/Autoload/ScoperAutoloadGeneratorTest.php b/tests/Autoload/ScoperAutoloadGeneratorTest.php index 9d61a2d27..a0d451648 100644 --- a/tests/Autoload/ScoperAutoloadGeneratorTest.php +++ b/tests/Autoload/ScoperAutoloadGeneratorTest.php @@ -14,35 +14,128 @@ namespace Humbug\PhpScoper\Autoload; +use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UserGlobalFunctionCollection; use Humbug\PhpScoper\Whitelist; +use PhpParser\Node\Name\FullyQualified; use PHPUnit\Framework\TestCase; class ScoperAutoloadGeneratorTest extends TestCase { - public function test_generate_the_autoload() + /** + * @dataProvider provideWhitelists + */ + public function test_generate_the_autoload(Whitelist $whitelist, string $expected) { - $whitelist = Whitelist::create(true, true,'A\Foo', 'B\Bar'); - $prefix = 'Humbug'; $generator = new ScoperAutoloadGenerator($whitelist); - $expected = <<<'PHP' + $actual = $generator->dump($prefix); + + $this->assertSame($expected, $actual); + } + + public function provideWhitelists() + { + yield [ + Whitelist::create(true, true,'A\Foo', 'B\Bar'), + <<<'PHP' dump($prefix); + yield [ + (function () { + $whitelist = Whitelist::create(true, true); - $this->assertSame($expected, $actual); + $userGlobalFunctions = new UserGlobalFunctionCollection(); + $userGlobalFunctions->add( + new FullyQualified('foo'), + new FullyQualified('Humbug\foo') + ); + $userGlobalFunctions->add( + new FullyQualified('bar'), + new FullyQualified('Humbug\bar') + ); + + $whitelist->recordUserGlobalFunction($userGlobalFunctions); + + return $whitelist; + })(), + <<<'PHP' +add( + new FullyQualified('foo'), + new FullyQualified('Humbug\foo') + ); + $userGlobalFunctions->add( + new FullyQualified('bar'), + new FullyQualified('Humbug\bar') + ); + + $whitelist->recordUserGlobalFunction($userGlobalFunctions); + + return $whitelist; + })(), + <<<'PHP' +