diff --git a/README.md b/README.md index a05a6dcf..f1e3b0e0 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ potentially very difficult to debug due to dissimilar or unsupported package ver - [Finders and paths](#finders-and-paths) - [Patchers](#patchers) - [Whitelist][whitelist] + - [Class Whitelisting](#class-whitelisting) + - [Namespace Whitelisting](#namespace-whitelisting) - [Building A Scoped PHAR](#building-a-scoped-phar) - [With Box](#with-box) - [Step 1: Configure build location and prep vendors](#step-1-configure-build-location-and-prep-vendors) @@ -258,7 +260,10 @@ the bundled code of your PHAR and the consumer code. For example if you have a PHPUnit PHAR with isolated code, you still want the PHAR to be able to understand the `PHPUnit\Framework\TestCase` class. -A way to achieve this is by specifying a list of classes to not prefix: + +### Class whitelisting + +You can whitelist classes and interfaces like so: ```php [ + 'PHPUnit\Framework\*', + ], +]; +``` + +Now anything under the `PHPUnit\Framework` namespace will not be prefixed. +Note this works as well for the global namespace: + +```php + [ + '*', + ], +]; +``` ## Building A Scoped PHAR diff --git a/specs/class-const/global-scope-single-level.php b/specs/class-const/global-scope-single-level.php index 8d658791..b41bf79f 100644 --- a/specs/class-const/global-scope-single-level.php +++ b/specs/class-const/global-scope-single-level.php @@ -43,6 +43,27 @@ class Command } \Humbug\Command::MAIN_CONST; +PHP + ], + + 'Constant call on a class belonging to the global namespace which is whitelisted: add root namespace statement' => [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Constant call on a namespaced class belonging to a whitelisted namespace: +- prefix the namespace +- prefix the class +- transforms the call into a FQ call to avoid autoloading issues +SPEC + , + 'whitelist' => ['X\PHPUnit\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Constant call on a namespaced class belonging to a whitelisted namespace (2): +- prefix the namespace +- prefix the class +- transforms the call into a FQ call to avoid autoloading issues +SPEC + , + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + [], ], - 'Declaration in the global namespace: add prefixed namespace.' => <<<'PHP' + 'Declaration in the global namespace: add prefixed namespace' => <<<'PHP' <<<'PHP' + 'Declaration in the global namespace with the global namespace whitelisted: add root namespace statement' => [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Declaration of a whitelisted class in the global namespace: +- add prefixed namespace +- append class alias statement to the class declaration +SPEC + , + 'whitelist' => ['A'], + 'payload' => <<<'PHP' + [ + 'whitelist' => ['A', '\*'], + 'payload' => <<<'PHP' + <<<'PHP' [ + [ + 'spec' => <<<'SPEC' +Declaration of a whitelisted class in the global namespace: +- add prefixed namespace +- append class alias statement to the class declaration +SPEC + , + 'whitelist' => ['A'], + 'payload' => <<<'PHP' + [ + 'whitelist' => ['Foo\*'], + 'payload' => <<<'PHP' + [ 'whitelist' => ['Foo\A'], 'payload' => <<<'PHP' <<<'SPEC' -Multiple declarations in different namespaces with whitelisted classes: prefix each namespace. -SPEC - , + 'Declaration of a whitelisted class with FQCN for the whitelist: append aliasing' => [ + 'whitelist' => ['\Foo\A'], + 'payload' => <<<'PHP' + [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + [ 'whitelist' => ['Foo\WA', 'Bar\WB', 'WC'], 'payload' => <<<'PHP' [], ], - 'Declaration in the global namespace: prefix non-internal classes.' => <<<'PHP' + 'Declaration in the global namespace: prefix non-internal classes' => <<<'PHP' <<<'PHP' + 'Declaration in the global namespace which is whitelisted: add root namespace statement' => [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Declaration in the global namespace with some whitelisted classes: +- add prefixed namespace +- prefix the classes +- append the class alias statements for the whitelisted class declarations +SPEC + , + 'whitelist' => ['A', 'C'], + 'payload' => <<<'PHP' + <<<'PHP' [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + <<<'PHP' + 'Catch a custom exception class' => <<<'PHP' <<<'PHP' + 'Catch a whitelisted custom exception class' => [ + 'whitelist' => ['FooException'], + 'payload' => <<<'PHP' + [ + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + <<<'PHP' [ + 'whitelist' => ['Acme\FooException'], + 'payload' => <<<'PHP' + [ + 'whitelist' => ['Acme\*'], + 'payload' => <<<'PHP' + <<<'PHP' <<<'SPEC' +Function declaration in a namespace: +- prefix the namespace statements +- prefix the appropriate classes +SPEC + , + 'whitelist' => ['X\Y'], + 'payload' => <<<'PHP' + <<<'SPEC' Function declaration in a namespace with use statements: @@ -191,6 +280,95 @@ function foo(\Humbug\Foo $arg0, \Humbug\Foo $arg1, \Humbug\Foo\Bar $arg2, \Humbu { } +PHP + ], + + [ + 'spec' => <<<'SPEC' +Function declaration in a whitelisted namespace: +- prefix the namespace statements +- prefix the appropriate classes +SPEC + , + 'whitelist' => ['Pi\*'], + 'payload' => <<<'PHP' + <<<'PHP' [ + 'whitelist' => ['Symfony\Component\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Use statement of a whitelisted class belonging to the global scope: +- wrap the statement in a prefixed namespace +- prefix the use statement +- append a class alias statement after the class declaration +SPEC + , + 'whitelist' => ['Foo'], + 'payload' => <<<'PHP' + <<<'SPEC' +Use statement of a class belonging to the global scope which has been whitelisted: +- wrap the statement in a prefixed namespace +- prefix the use statement +- append a class alias statement after the class declaration +SPEC + , + 'whitelist' => ['\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Use statement of a whitelisted class belonging to the global scope which has been whitelisted: +- wrap the statement in a prefixed namespace +- prefix the use statement +- append a class alias statement after the class declaration +SPEC + , + 'whitelist' => ['Foo', '\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Use statement of two-level class belonging to a whitelisted namespace: +- prefix the namespaces +- prefix the use statement +SPEC + , + 'whitelist' => ['Foo\*'], + 'payload' => <<<'PHP' + <<<'SPEC' +Use statement of whitelisted two-level class belonging to a whitelisted namespace: +- prefix the namespaces +- prefix the use statement +SPEC + , + 'whitelist' => ['Foo', 'Foo\*'], + 'payload' => <<<'PHP' +path = $path; $this->prefix = $prefix; @@ -162,7 +162,7 @@ public function getPatchers(): array return $this->patchers; } - public function getWhitelist(): array + public function getWhitelist(): Whitelist { return $this->whitelist; } @@ -239,10 +239,10 @@ private static function retrievePatchers(array $config): array return $patchers; } - private static function retrieveWhitelist(array $config): array + private static function retrieveWhitelist(array $config): Whitelist { if (false === array_key_exists(self::WHITELIST_KEYWORD, $config)) { - return []; + return Whitelist::create(); } $whitelist = $config[self::WHITELIST_KEYWORD]; @@ -269,7 +269,7 @@ private static function retrieveWhitelist(array $config): array ); } - return $whitelist; + return Whitelist::create(...$whitelist); } private static function retrieveFinders(array $config): array diff --git a/src/Console/Command/AddPrefixCommand.php b/src/Console/Command/AddPrefixCommand.php index e9556b89..7b3189e8 100644 --- a/src/Console/Command/AddPrefixCommand.php +++ b/src/Console/Command/AddPrefixCommand.php @@ -19,6 +19,7 @@ use Humbug\PhpScoper\Logger\ConsoleLogger; use Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Throwable\Exception\ParsingException; +use Humbug\PhpScoper\Whitelist; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -165,12 +166,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + /** + * @var callable[] + */ private function scopeFiles( string $prefix, array $filesWithContents, string $output, array $patchers, - array $whitelist, + Whitelist $whitelist, bool $stopOnFailure, ConsoleLogger $logger ): void { @@ -220,14 +224,7 @@ function ($a, $b) { } /** - * @param string $inputFilePath - * @param string $outputFilePath - * @param string $inputContents - * @param string $prefix - * @param callable[] $patchers - * @param string[] $whitelist - * @param bool $stopOnFailure - * @param ConsoleLogger $logger + * @param callable[] $patchers */ private function scopeFile( string $inputFilePath, @@ -235,7 +232,7 @@ private function scopeFile( string $outputFilePath, string $prefix, array $patchers, - array $whitelist, + Whitelist $whitelist, bool $stopOnFailure, ConsoleLogger $logger ): void { diff --git a/src/PhpParser/NodeVisitor/NameStmtPrefixer.php b/src/PhpParser/NodeVisitor/NameStmtPrefixer.php index af4c790f..91445b9e 100644 --- a/src/PhpParser/NodeVisitor/NameStmtPrefixer.php +++ b/src/PhpParser/NodeVisitor/NameStmtPrefixer.php @@ -16,6 +16,7 @@ use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\FullyQualifiedNameResolver; use Humbug\PhpScoper\Reflector; +use Humbug\PhpScoper\Whitelist; use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\ConstFetch; @@ -58,20 +59,18 @@ final class NameStmtPrefixer extends NodeVisitorAbstract ]; private $prefix; + private $whitelist; private $nameResolver; private $reflector; - /** - * @param string $prefix - * @param FullyQualifiedNameResolver $nameResolver - * @param Reflector $reflector - */ public function __construct( string $prefix, + Whitelist $whitelist, FullyQualifiedNameResolver $nameResolver, Reflector $reflector ) { $this->prefix = $prefix; + $this->whitelist = $whitelist; $this->nameResolver = $nameResolver; $this->reflector = $reflector; } @@ -147,6 +146,11 @@ private function prefixName(Name $name): Node return $resolvedName; } + // Skip if the node namespace is whitelisted + if ($this->whitelist->isNamespaceWhitelisted((string) $resolvedName)) { + return $resolvedName; + } + // Check if the class can be prefixed if (false === ($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall)) { if ($this->reflector->isClassInternal($resolvedName->toString())) { diff --git a/src/PhpParser/NodeVisitor/NamespaceStmtPrefixer.php b/src/PhpParser/NodeVisitor/NamespaceStmtPrefixer.php index d4a042d5..14016cf0 100644 --- a/src/PhpParser/NodeVisitor/NamespaceStmtPrefixer.php +++ b/src/PhpParser/NodeVisitor/NamespaceStmtPrefixer.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\PhpParser\NodeVisitor; use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\NamespaceStmtCollection; +use Humbug\PhpScoper\Whitelist; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; @@ -41,11 +42,13 @@ final class NamespaceStmtPrefixer extends NodeVisitorAbstract { private $prefix; + private $whitelist; private $namespaceStatements; - public function __construct(string $prefix, NamespaceStmtCollection $namespaceStatements) + public function __construct(string $prefix, Whitelist $whitelist, NamespaceStmtCollection $namespaceStatements) { $this->prefix = $prefix; + $this->whitelist = $whitelist; $this->namespaceStatements = $namespaceStatements; } @@ -75,7 +78,7 @@ private function prefixNamespaceStmt(Namespace_ $namespace): Node return $namespace; } - private function isWhitelistedNode(Node $node) + private function isWhitelistedNode(Node $node): bool { if (($node instanceof Class_ || $node instanceof Interface_)) { return true; @@ -95,6 +98,10 @@ private function isWhitelistedNode(Node $node) private function shouldPrefixStmt(Namespace_ $namespace): bool { + if ($this->whitelist->isNamespaceWhitelisted((string) $namespace->name)) { + return false; + } + return null === $namespace->name || (null !== $namespace->name && $this->prefix !== $namespace->name->getFirst()); } } diff --git a/src/PhpParser/NodeVisitor/StringScalarPrefixer.php b/src/PhpParser/NodeVisitor/StringScalarPrefixer.php index 77840367..ec40e430 100644 --- a/src/PhpParser/NodeVisitor/StringScalarPrefixer.php +++ b/src/PhpParser/NodeVisitor/StringScalarPrefixer.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\PhpParser\NodeVisitor; use Humbug\PhpScoper\Reflector; +use Humbug\PhpScoper\Whitelist; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Const_; @@ -50,11 +51,13 @@ final class StringScalarPrefixer extends NodeVisitorAbstract { private $prefix; + private $whitelist; private $reflector; - public function __construct(string $prefix, Reflector $reflector) + public function __construct(string $prefix, Whitelist $whitelist, Reflector $reflector) { $this->prefix = $prefix; + $this->whitelist = $whitelist; $this->reflector = $reflector; } @@ -109,10 +112,12 @@ private function prefixStringScalar(String_ $string): Node // Skip if is already prefixed if ($this->prefix === $stringName->getFirst()) { $newStringName = $stringName; - // Check if the class can be prefixed: class from the global namespace + // Check if the class can be prefixed: class not from the global namespace or which the namespace is not + // whitelisted } elseif ( 1 === count($stringName->parts) || $this->reflector->isClassInternal($stringName->toString()) + || $this->whitelist->isNamespaceWhitelisted((string) $stringName) ) { $newStringName = $stringName; } else { diff --git a/src/PhpParser/NodeVisitor/UseStmt/UseStmtPrefixer.php b/src/PhpParser/NodeVisitor/UseStmt/UseStmtPrefixer.php index 6d6b340b..272f8bf9 100644 --- a/src/PhpParser/NodeVisitor/UseStmt/UseStmtPrefixer.php +++ b/src/PhpParser/NodeVisitor/UseStmt/UseStmtPrefixer.php @@ -16,6 +16,7 @@ use Humbug\PhpScoper\PhpParser\NodeVisitor\AppendParentNode; use Humbug\PhpScoper\Reflector; +use Humbug\PhpScoper\Whitelist; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Use_; @@ -30,12 +31,14 @@ final class UseStmtPrefixer extends NodeVisitorAbstract { private $prefix; + private $whitelist; private $reflector; - public function __construct(string $prefix, Reflector $reflector) + public function __construct(string $prefix, Whitelist $whitelist, Reflector $reflector) { $this->prefix = $prefix; $this->reflector = $reflector; + $this->whitelist = $whitelist; } /** @@ -59,6 +62,11 @@ private function shouldPrefixUseStmt(UseUse $use): bool return false; } + // If is whitelisted + if ($this->whitelist->isNamespaceWhitelisted((string) $use->name)) { + return false; + } + if (Use_::TYPE_FUNCTION === $useType) { return false === $this->reflector->isFunctionInternal((string) $use->name); } diff --git a/src/PhpParser/NodeVisitor/WhitelistedClassAppender.php b/src/PhpParser/NodeVisitor/WhitelistedClassAppender.php index 84821c66..0315e356 100644 --- a/src/PhpParser/NodeVisitor/WhitelistedClassAppender.php +++ b/src/PhpParser/NodeVisitor/WhitelistedClassAppender.php @@ -14,6 +14,7 @@ namespace Humbug\PhpScoper\PhpParser\NodeVisitor; +use Humbug\PhpScoper\Whitelist; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\ConstFetch; @@ -56,12 +57,9 @@ final class WhitelistedClassAppender extends NodeVisitorAbstract { private $whitelist; - /** - * @param string[] $whitelists - */ - public function __construct(array $whitelists) + public function __construct(Whitelist $whitelist) { - $this->whitelist = $whitelists; + $this->whitelist = $whitelist; } /** @@ -90,10 +88,13 @@ private function appendToNamespaceStmt(Namespace_ $namespace): Namespace_ } /** @var Class_ $stmt */ - $name = FullyQualified::concat((string) $namespace->name, (string) $stmt->name); + $name = null === $namespace->name + ? new FullyQualified((string) $stmt->name, $stmt->getAttributes()) + : FullyQualified::concat((string) $namespace->name, (string) $stmt->name) + ; $originalName = $name->slice(1); - if (false === in_array((string) $originalName, $this->whitelist, true)) { + if (false === $this->whitelist->isClassWhitelisted((string) $originalName)) { continue; } diff --git a/src/PhpParser/TraverserFactory.php b/src/PhpParser/TraverserFactory.php index 337cd07c..b5cfbaa7 100644 --- a/src/PhpParser/TraverserFactory.php +++ b/src/PhpParser/TraverserFactory.php @@ -18,6 +18,7 @@ use Humbug\PhpScoper\PhpParser\NodeVisitor\Collection\UseStmtCollection; use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\FullyQualifiedNameResolver; use Humbug\PhpScoper\Reflector; +use Humbug\PhpScoper\Whitelist; use PhpParser\NodeTraverserInterface; /** @@ -32,7 +33,7 @@ public function __construct(Reflector $reflector) $this->reflector = $reflector; } - public function create(string $prefix, array $whitelist): NodeTraverserInterface + public function create(string $prefix, Whitelist $whitelist): NodeTraverserInterface { $traverser = new NodeTraverser($prefix); @@ -43,13 +44,13 @@ public function create(string $prefix, array $whitelist): NodeTraverserInterface $traverser->addVisitor(new NodeVisitor\AppendParentNode()); - $traverser->addVisitor(new NodeVisitor\NamespaceStmtPrefixer($prefix, $namespaceStatements)); + $traverser->addVisitor(new NodeVisitor\NamespaceStmtPrefixer($prefix, $whitelist, $namespaceStatements)); $traverser->addVisitor(new NodeVisitor\UseStmt\UseStmtCollector($namespaceStatements, $useStatements)); - $traverser->addVisitor(new NodeVisitor\UseStmt\UseStmtPrefixer($prefix, $this->reflector)); + $traverser->addVisitor(new NodeVisitor\UseStmt\UseStmtPrefixer($prefix, $whitelist, $this->reflector)); - $traverser->addVisitor(new NodeVisitor\NameStmtPrefixer($prefix, $nameResolver, $this->reflector)); - $traverser->addVisitor(new NodeVisitor\StringScalarPrefixer($prefix, $this->reflector)); + $traverser->addVisitor(new NodeVisitor\NameStmtPrefixer($prefix, $whitelist, $nameResolver, $this->reflector)); + $traverser->addVisitor(new NodeVisitor\StringScalarPrefixer($prefix, $whitelist, $this->reflector)); $traverser->addVisitor(new NodeVisitor\WhitelistedClassAppender($whitelist)); diff --git a/src/Scoper.php b/src/Scoper.php index d468e898..365a5e33 100644 --- a/src/Scoper.php +++ b/src/Scoper.php @@ -25,11 +25,11 @@ interface Scoper * @param string $contents File contents * @param string $prefix Prefix to apply to the file * @param callable[] $patchers - * @param string[] $whitelist List of classes to exclude from the scoping. + * @param Whitelist $whitelist List of classes to exclude from the scoping. * * @throws ParsingException * * @return string Contents of the file with the prefix applied */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string; + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string; } diff --git a/src/Scoper/Composer/InstalledPackagesScoper.php b/src/Scoper/Composer/InstalledPackagesScoper.php index 4e662a22..51d35d3d 100644 --- a/src/Scoper/Composer/InstalledPackagesScoper.php +++ b/src/Scoper/Composer/InstalledPackagesScoper.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\Scoper\Composer; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; final class InstalledPackagesScoper implements Scoper { @@ -32,7 +33,7 @@ public function __construct(Scoper $decoratedScoper) * * {@inheritdoc} */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string { if (1 !== preg_match(self::$filePattern, $filePath)) { return $this->decoratedScoper->scope($filePath, $contents, $prefix, $patchers, $whitelist); diff --git a/src/Scoper/Composer/JsonFileScoper.php b/src/Scoper/Composer/JsonFileScoper.php index 1e2d9f2c..fd8317bd 100644 --- a/src/Scoper/Composer/JsonFileScoper.php +++ b/src/Scoper/Composer/JsonFileScoper.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\Scoper\Composer; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; final class JsonFileScoper implements Scoper { @@ -30,7 +31,7 @@ public function __construct(Scoper $decoratedScoper) * * {@inheritdoc} */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string { if (1 !== preg_match('/composer\.json$/', $filePath)) { return $this->decoratedScoper->scope($filePath, $contents, $prefix, $patchers, $whitelist); diff --git a/src/Scoper/NullScoper.php b/src/Scoper/NullScoper.php index 306c34cc..3ccfb637 100644 --- a/src/Scoper/NullScoper.php +++ b/src/Scoper/NullScoper.php @@ -15,13 +15,14 @@ namespace Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; final class NullScoper implements Scoper { /** * @inheritdoc */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string { return $contents; } diff --git a/src/Scoper/PatchScoper.php b/src/Scoper/PatchScoper.php index 3a523b51..c05a23ae 100644 --- a/src/Scoper/PatchScoper.php +++ b/src/Scoper/PatchScoper.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; final class PatchScoper implements Scoper { @@ -28,7 +29,7 @@ public function __construct(Scoper $decoratedScoper) /** * @inheritdoc */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string { $contents = $this->decoratedScoper->scope($filePath, $contents, $prefix, $patchers, $whitelist); diff --git a/src/Scoper/PhpScoper.php b/src/Scoper/PhpScoper.php index e6923b7c..ce465edc 100644 --- a/src/Scoper/PhpScoper.php +++ b/src/Scoper/PhpScoper.php @@ -16,6 +16,7 @@ use Humbug\PhpScoper\PhpParser\TraverserFactory; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; use PhpParser\Error as PhpParserError; use PhpParser\Parser; use PhpParser\PrettyPrinter\Standard; @@ -45,7 +46,7 @@ public function __construct(Parser $parser, Scoper $decoratedScoper, TraverserFa * * @throws PhpParserError */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string { if (false === $this->isPhpFile($filePath, $contents)) { return $this->decoratedScoper->scope($filePath, $contents, $prefix, $patchers, $whitelist); diff --git a/src/Whitelist.php b/src/Whitelist.php new file mode 100644 index 00000000..8d5f3c92 --- /dev/null +++ b/src/Whitelist.php @@ -0,0 +1,94 @@ +, + * 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; + +use Countable; +use InvalidArgumentException; +use function count; +use function in_array; +use function sprintf; +use function substr; +use function trim; + +final class Whitelist implements Countable +{ + private $classes; + private $namespaces; + + public static function create(string ...$elements): self + { + $classes = []; + $namespaces = []; + + foreach ($elements as $element) { + if (isset($element[0]) && '\\' === $element[0]) { + $element = substr($element, 1); + } + + if ('' === trim($element)) { + throw new InvalidArgumentException( + sprintf( + 'Invalid whitelist element "%s": cannot accept an empty string', + $element + ) + ); + } + + if ('\*' === substr($element, -2)) { + $namespaces[] = substr($element, 0, -2); + } elseif ('*' === $element) { + $namespaces[] = ''; + } else { + $classes[] = $element; + } + } + + return new self($classes, $namespaces); + } + + /** + * @param string[] $classes + * @param string[] $namespaces + */ + private function __construct(array $classes, array $namespaces) + { + $this->classes = $classes; + $this->namespaces = $namespaces; + } + + public function isClassWhitelisted(string $name): bool + { + return in_array($name, $this->classes, true); + } + + public function isNamespaceWhitelisted(string $name): bool + { + foreach ($this->namespaces as $namespace) { + if ('' === $namespace || 0 === strpos($name, $namespace)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return count($this->classes) + count($this->namespaces); + } +} diff --git a/tests/Console/Command/AddPrefixCommandTest.php b/tests/Console/Command/AddPrefixCommandTest.php index 469d8cef..2796bc4d 100644 --- a/tests/Console/Command/AddPrefixCommandTest.php +++ b/tests/Console/Command/AddPrefixCommandTest.php @@ -17,6 +17,7 @@ use Humbug\PhpScoper\Console\Application; use Humbug\PhpScoper\FileSystemTestCase; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; use InvalidArgumentException; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; @@ -173,7 +174,7 @@ public function test_scope_the_given_paths() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -236,7 +237,7 @@ public function test_let_the_file_unchanged_when_cannot_scope_a_file() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -249,7 +250,7 @@ public function test_let_the_file_unchanged_when_cannot_scope_a_file() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willThrow(new \RuntimeException('Scoping of the file failed')) ; @@ -309,7 +310,7 @@ public function test_do_not_scope_duplicated_given_paths() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -370,7 +371,7 @@ public function test_scope_the_given_paths_and_the_ones_found_by_the_finder() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedFileContents) ; @@ -424,7 +425,7 @@ function (string $prefix): bool { } ), [], - [] + Whitelist::create() ) ->willReturn('') ; @@ -482,7 +483,7 @@ public function test_scope_the_current_working_directory_if_no_path_given() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -528,7 +529,7 @@ public function test_prefix_can_end_by_a_backslash() Argument::any(), 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn('') ; @@ -571,7 +572,7 @@ public function test_prefix_can_end_by_multiple_backslashes() Argument::any(), 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn('') ; @@ -629,7 +630,7 @@ public function test_an_output_directory_can_be_given() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -691,7 +692,7 @@ public function test_relative_output_directory_are_made_absolute() $inputContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -782,7 +783,7 @@ public function test_attempts_to_use_patch_file_in_current_directory() return true; }), - [] + Whitelist::create() ) ->willReturn($prefixedContents) ; @@ -867,7 +868,7 @@ public function test_can_scope_projects_with_invalid_files() $fileContents, 'MyPrefix', [], - [] + Whitelist::create() ) ->willThrow($scopingException = new RuntimeException('Could not scope file')) ; diff --git a/tests/PhpParser/TraverserFactoryTest.php b/tests/PhpParser/TraverserFactoryTest.php index 8595b275..b1de55e2 100644 --- a/tests/PhpParser/TraverserFactoryTest.php +++ b/tests/PhpParser/TraverserFactoryTest.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\PhpParser; use Humbug\PhpScoper\Reflector; +use Humbug\PhpScoper\Whitelist; use PHPUnit\Framework\TestCase; use Roave\BetterReflection\BetterReflection; @@ -27,7 +28,7 @@ public function test_creates_a_new_traverser_at_each_call() { $prefix = 'Humbug'; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $classReflector = new Reflector( (new BetterReflection())->classReflector(), diff --git a/tests/Scoper/Composer/InstalledPackagesScoperTest.php b/tests/Scoper/Composer/InstalledPackagesScoperTest.php index be9dfc8a..58f0ac37 100644 --- a/tests/Scoper/Composer/InstalledPackagesScoperTest.php +++ b/tests/Scoper/Composer/InstalledPackagesScoperTest.php @@ -16,6 +16,7 @@ use Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper\FakeScoper; +use Humbug\PhpScoper\Whitelist; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; @@ -38,7 +39,7 @@ public function test_delegates_scoping_to_the_decorated_scoper_if_is_not_a_insta $fileContents = ''; $prefix = 'Humbug'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); /** @var Scoper|ObjectProphecy $decoratedScoperProphecy */ $decoratedScoperProphecy = $this->prophesize(Scoper::class); @@ -71,7 +72,7 @@ public function test_it_prefixes_the_composer_autoloaders(string $fileContents, $prefix = 'Foo'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $actual = $scoper->scope($filePath, $fileContents, $prefix, $patchers, $whitelist); diff --git a/tests/Scoper/Composer/JsonFileScoperTest.php b/tests/Scoper/Composer/JsonFileScoperTest.php index 87e898f8..99208304 100644 --- a/tests/Scoper/Composer/JsonFileScoperTest.php +++ b/tests/Scoper/Composer/JsonFileScoperTest.php @@ -16,6 +16,7 @@ use Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper\FakeScoper; +use Humbug\PhpScoper\Whitelist; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; @@ -38,7 +39,7 @@ public function test_delegates_scoping_to_the_decorated_scoper_if_is_not_a_compo $fileContents = ''; $prefix = 'Humbug'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); /** @var Scoper|ObjectProphecy $decoratedScoperProphecy */ $decoratedScoperProphecy = $this->prophesize(Scoper::class); @@ -71,7 +72,7 @@ public function test_it_prefixes_the_composer_autoloaders(string $fileContents, $prefix = 'Foo'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $actual = $scoper->scope($filePath, $fileContents, $prefix, $patchers, $whitelist); @@ -145,7 +146,7 @@ public function test_it_prefixes_psr0_autoloaders(string $fileContents, string $ $prefix = 'Foo'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $actual = $scoper->scope($filePath, $fileContents, $prefix, $patchers, $whitelist); diff --git a/tests/Scoper/FakeScoper.php b/tests/Scoper/FakeScoper.php index a32eea0b..8b8ce382 100644 --- a/tests/Scoper/FakeScoper.php +++ b/tests/Scoper/FakeScoper.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; use LogicException; final class FakeScoper implements Scoper @@ -22,7 +23,7 @@ final class FakeScoper implements Scoper /** * @inheritdoc */ - public function scope(string $filePath, string $contents, string $prefix, array $patchers, array $whitelist): string + public function scope(string $filePath, string $contents, string $prefix, array $patchers, Whitelist $whitelist): string { throw new LogicException(); } diff --git a/tests/Scoper/NullScoperTest.php b/tests/Scoper/NullScoperTest.php index 95c58ebc..5212f815 100644 --- a/tests/Scoper/NullScoperTest.php +++ b/tests/Scoper/NullScoperTest.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; use PHPUnit\Framework\TestCase; use function Humbug\PhpScoper\create_fake_patcher; @@ -37,7 +38,7 @@ public function test_returns_the_file_content_unchanged() $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $scoper = new NullScoper(); diff --git a/tests/Scoper/PatchScoperTest.php b/tests/Scoper/PatchScoperTest.php index 2dd67d4f..ad70650e 100644 --- a/tests/Scoper/PatchScoperTest.php +++ b/tests/Scoper/PatchScoperTest.php @@ -15,6 +15,7 @@ namespace Humbug\PhpScoper\Scoper; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -72,7 +73,7 @@ function (string $patcherFilePath, string $patcherPrefix, string $contents) use }, ]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $this->decoratedScoperProphecy ->scope($filePath, $contents, $prefix, $patchers, $whitelist) diff --git a/tests/Scoper/PhpScoperTest.php b/tests/Scoper/PhpScoperTest.php index 218c0443..f2c24a05 100644 --- a/tests/Scoper/PhpScoperTest.php +++ b/tests/Scoper/PhpScoperTest.php @@ -19,6 +19,7 @@ use Humbug\PhpScoper\PhpParser\TraverserFactory; use Humbug\PhpScoper\Reflector; use Humbug\PhpScoper\Scoper; +use Humbug\PhpScoper\Whitelist; use LogicException; use PhpParser\Error as PhpParserError; use PhpParser\Node\Name; @@ -153,7 +154,7 @@ public function test_can_scope_a_PHP_file() $prefix = 'Humbug'; $filePath = 'file.php'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $contents = <<<'PHP' decoratedScoperProphecy ->scope($filePath, $fileContents, $prefix, $patchers, $whitelist) @@ -213,7 +214,7 @@ public function test_can_scope_a_PHP_file_with_the_wrong_extension() $prefix = 'Humbug'; $filePath = 'file'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $contents = <<<'PHP' scoper->scope($filePath, $contents, $prefix, $patchers, $whitelist); @@ -343,7 +344,7 @@ public function test_creates_a_new_traverser_for_each_file() $prefix = 'Humbug'; $patchers = [create_fake_patcher()]; - $whitelist = ['Foo']; + $whitelist = Whitelist::create('Foo'); $this->decoratedScoperProphecy ->scope(Argument::any(), Argument::any(), $prefix, $patchers, $whitelist) @@ -419,7 +420,7 @@ function (...$args) use (&$i): bool { /** * @dataProvider provideValidFiles */ - public function test_can_scope_valid_files(string $spec, string $contents, string $prefix, array $whitelist, string $expected) + public function test_can_scope_valid_files(string $spec, string $contents, string $prefix, Whitelist $whitelist, string $expected) { $filePath = 'file.php'; @@ -541,7 +542,7 @@ private function parseSpecFile(array $meta, $fixtureTitle, $fixtureSet): Generat $spec, $payloadParts[0], // Input $fixtureSet['prefix'] ?? $meta['prefix'], - $fixtureSet['whitelist'] ?? $meta['whitelist'], + Whitelist::create(...($fixtureSet['whitelist'] ?? $meta['whitelist'])), $payloadParts[1], // Expected output ]; } diff --git a/tests/WhitelistTest.php b/tests/WhitelistTest.php new file mode 100644 index 00000000..f0e282b9 --- /dev/null +++ b/tests/WhitelistTest.php @@ -0,0 +1,164 @@ +, + * 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; + +use PHPUnit\Framework\TestCase; +use Reflection; +use ReflectionClass; + +/** + * @covers \Humbug\PhpScoper\Whitelist + */ +class WhitelistTest extends TestCase +{ + /** + * @dataProvider provideWhitelists + */ + public function test_it_can_be_created_from_a_list_of_strings( + array $whitelist, + array $expectedClasses, + array $expectedNamespaces + ) { + $whitelistObject = Whitelist::create(...$whitelist); + + $whitelistReflection = new ReflectionClass(Whitelist::class); + + $whitelistClassReflection = $whitelistReflection->getProperty('classes'); + $whitelistClassReflection->setAccessible(true); + $actualClasses = $whitelistClassReflection->getValue($whitelistObject); + + $whitelistNamespaceReflection = $whitelistReflection->getProperty('namespaces'); + $whitelistNamespaceReflection->setAccessible(true); + $actualNamespaces = $whitelistNamespaceReflection->getValue($whitelistObject); + + $this->assertSame($expectedClasses, $actualClasses); + $this->assertSame($expectedNamespaces, $actualNamespaces); + } + + /** + * @dataProvider provideClassWhitelists + */ + public function test_it_can_tell_if_a_class_is_whitelisted(Whitelist $whitelist, string $class, bool $expected) + { + $actual = $whitelist->isClassWhitelisted($class); + + $this->assertSame($expected, $actual); + } + + /** + * @dataProvider provideNamespaceWhitelists + */ + public function test_it_can_tell_if_a_namespace_is_whitelisted(Whitelist $whitelist, string $class, bool $expected) + { + $actual = $whitelist->isNamespaceWhitelisted($class); + + $this->assertSame($expected, $actual); + } + + public function provideWhitelists() + { + yield [[], [], []]; + + yield [['Acme\Foo'], ['Acme\Foo'], []]; + + yield [['\Acme\Foo'], ['Acme\Foo'], []]; + + yield [['Acme\Foo\*'], [], ['Acme\Foo']]; + + yield [['\*'], [], ['']]; + + yield [['*'], [], ['']]; + + yield [['Acme\Foo', 'Acme\Foo\*', '\*'], ['Acme\Foo'], ['Acme\Foo', '']]; + } + + public function provideClassWhitelists() + { + yield [ + Whitelist::create(), + 'Acme\Foo', + false, + ]; + + yield [ + Whitelist::create('Acme\Foo'), + 'Acme\Foo', + true, + ]; + + yield [ + Whitelist::create('Acme\Foo'), + 'Acme\Foo\Bar', + false, + ]; + + yield [ + Whitelist::create('Acme\Foo'), + 'Acme', + false, + ]; + + yield [ + Whitelist::create('Acme'), + 'Acme', + true, + ]; + + yield [ + Whitelist::create('Acme\*'), + 'Acme', + false, + ]; + } + + public function provideNamespaceWhitelists() + { + yield [ + Whitelist::create(), + 'Acme\Foo', + false, + ]; + + yield [ + Whitelist::create('Acme\Foo\*'), + 'Acme\Foo', + true, + ]; + + yield [ + Whitelist::create('Acme\*'), + 'Acme\Foo', + true, + ]; + + yield [ + Whitelist::create('Acme\Foo\*'), + 'Acme\Foo\Bar', + true, + ]; + + yield [ + Whitelist::create('\*'), + 'Acme', + true, + ]; + + yield [ + Whitelist::create('\*'), + 'Acme\Foo', + true, + ]; + } +}