mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 14:23:34 +02:00
fix: don't allow invalid class names
This commit is contained in:
@@ -90,11 +90,11 @@ final class TestSuiteLoader
|
|||||||
(static function () use ($suiteClassFile) {
|
(static function () use ($suiteClassFile) {
|
||||||
try {
|
try {
|
||||||
include_once $suiteClassFile;
|
include_once $suiteClassFile;
|
||||||
|
|
||||||
|
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Panic::with($e);
|
Panic::with($e);
|
||||||
}
|
}
|
||||||
|
|
||||||
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
$loadedClasses = array_values(
|
$loadedClasses = array_values(
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class AfterAllAlreadyExist extends InvalidArgumentException implements Exc
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The afterAll already exists in the filename `%s`.', $filename));
|
parent::__construct(sprintf('The afterAll already exists in the filename [%s].', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class AfterAllWithinDescribe extends InvalidArgumentException implements E
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename `%s`.', $filename));
|
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename [%s].', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class AfterBeforeTestFunction extends InvalidArgumentException implements
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.');
|
parent::__construct('After method cannot be used with before the [test|it] functions in the filename ['.$filename.'].');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class BeforeAllAlreadyExist extends InvalidArgumentException implements Ex
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The beforeAll already exists in the filename `%s`.', $filename));
|
parent::__construct(sprintf('The beforeAll already exists in the filename [%s].', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class BeforeAllWithinDescribe extends InvalidArgumentException implements
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename `%s`.', $filename));
|
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename [%s].', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class DatasetAlreadyExists extends InvalidArgumentException implements Exc
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $name, string $scope)
|
public function __construct(string $name, string $scope)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('A dataset with the name `%s` already exists in scope [%s].', $name, $scope));
|
parent::__construct(sprintf('A dataset with the name [%s] already exists in scope [%s].', $name, $scope));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class DatasetDoesNotExist extends InvalidArgumentException implements Exce
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $name)
|
public function __construct(string $name)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf("A dataset with the name `%s` does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name));
|
parent::__construct(sprintf("A dataset with the name [%s] does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class FileOrFolderNotFound extends InvalidArgumentException implements Exc
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The file or folder with the name `%s` could not be found.', $filename));
|
parent::__construct(sprintf('The file or folder with the name [%s] could not be found.', $filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||||
|
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class InvalidTestClassName extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new Exception instance for the given class name.
|
||||||
|
*/
|
||||||
|
public static function fromClassName(string $filename, string $className): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'The test file [%s] would create the class [%s], which is not a valid PHP class name. Please rename the test file.',
|
||||||
|
$filename,
|
||||||
|
$className,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Exception instance for the given namespace.
|
||||||
|
*/
|
||||||
|
public static function fromNamespace(string $filename, string $namespace, string $part): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'The test file [%s] would create the namespace [%s], which is not a valid PHP namespace, as [%s] may not be used as a namespace name. Please rename the folder in question.',
|
||||||
|
$filename,
|
||||||
|
$namespace,
|
||||||
|
$part,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,6 @@ final class TestAlreadyExist extends InvalidArgumentException implements Excepti
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $fileName, string $description)
|
public function __construct(string $fileName, string $description)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('A test with the description `%s` already exists in the filename `%s`.', $description, $fileName));
|
parent::__construct(sprintf('A test with the description [%s] already exists in the filename [%s].', $description, $fileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implem
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $testCaseClass)
|
public function __construct(string $testCaseClass)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('The class `%s` was not found.', $testCaseClass));
|
parent::__construct(sprintf('The class [%s] was not found.', $testCaseClass));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ final class TestDescriptionMissing extends InvalidArgumentException implements E
|
|||||||
*/
|
*/
|
||||||
public function __construct(string $fileName)
|
public function __construct(string $fileName)
|
||||||
{
|
{
|
||||||
parent::__construct(sprintf('Test description is missing in the filename `%s`.', $fileName));
|
parent::__construct(sprintf('Test description is missing in the filename [%s].', $fileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Pest\Concerns;
|
|||||||
use Pest\Contracts\HasPrintableTestCaseName;
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
use Pest\Evaluators\Attributes;
|
use Pest\Evaluators\Attributes;
|
||||||
use Pest\Exceptions\DatasetMissing;
|
use Pest\Exceptions\DatasetMissing;
|
||||||
|
use Pest\Exceptions\InvalidTestClassName;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\Exceptions\TestAlreadyExist;
|
use Pest\Exceptions\TestAlreadyExist;
|
||||||
use Pest\Exceptions\TestClosureMustNotBeStatic;
|
use Pest\Exceptions\TestClosureMustNotBeStatic;
|
||||||
@@ -138,6 +139,16 @@ final class TestCaseFactory
|
|||||||
|
|
||||||
if (trim($className) === '') {
|
if (trim($className) === '') {
|
||||||
$className = 'InvalidTestName'.Str::random();
|
$className = 'InvalidTestName'.Str::random();
|
||||||
|
} elseif (! Str::isValidClassName($className)) {
|
||||||
|
throw InvalidTestClassName::fromClassName($this->filename, $className);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->namespace === null) {
|
||||||
|
foreach ($partsFQN as $partFQN) {
|
||||||
|
if (! Str::isValidIdentifier($partFQN)) {
|
||||||
|
throw InvalidTestClassName::fromNamespace($this->filename, $namespace, $partFQN);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->attributes = [
|
$this->attributes = [
|
||||||
|
|||||||
@@ -17,6 +17,33 @@ final class Str
|
|||||||
|
|
||||||
private const string PREFIX = '__pest_evaluable_';
|
private const string PREFIX = '__pest_evaluable_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of names PHP reserves, and therefore refuses, as class names.
|
||||||
|
*
|
||||||
|
* @see https://github.com/php/php-src/blob/master/Zend/zend_compile.c
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const array RESERVED_CLASS_NAMES = [
|
||||||
|
'array',
|
||||||
|
'bool',
|
||||||
|
'callable',
|
||||||
|
'false',
|
||||||
|
'float',
|
||||||
|
'int',
|
||||||
|
'iterable',
|
||||||
|
'mixed',
|
||||||
|
'never',
|
||||||
|
'null',
|
||||||
|
'object',
|
||||||
|
'parent',
|
||||||
|
'self',
|
||||||
|
'static',
|
||||||
|
'string',
|
||||||
|
'true',
|
||||||
|
'void',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
||||||
* string value.
|
* string value.
|
||||||
@@ -64,6 +91,35 @@ final class Str
|
|||||||
return (string) preg_replace('/[^a-zA-Z0-9_\x80-\xff]/', '_', $code);
|
return (string) preg_replace('/[^a-zA-Z0-9_\x80-\xff]/', '_', $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given name is a valid PHP identifier, and therefore may
|
||||||
|
* be used as a single namespace name.
|
||||||
|
*/
|
||||||
|
public static function isValidIdentifier(string $name): bool
|
||||||
|
{
|
||||||
|
return preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $name) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given name may be declared as a class name by an `eval`.
|
||||||
|
*/
|
||||||
|
public static function isValidClassName(string $name): bool
|
||||||
|
{
|
||||||
|
if (! self::isValidIdentifier($name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(strtolower($name), self::RESERVED_CLASS_NAMES, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = token_get_all(sprintf('<?php %s;', $name));
|
||||||
|
|
||||||
|
// Anything the lexer sees as a keyword, like `list` or `match`, may not
|
||||||
|
// be used as a class name.
|
||||||
|
return is_array($tokens[1] ?? null) && $tokens[1][0] === T_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the portion of a string before the last occurrence of a given value.
|
* Get the portion of a string before the last occurrence of a given value.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ test('reports missing datasets as errors for a single file run', function () use
|
|||||||
$result = $run('tests/Fixtures/Suites/IssueOnly.php');
|
$result = $run('tests/Fixtures/Suites/IssueOnly.php');
|
||||||
|
|
||||||
expect($result['output'])
|
expect($result['output'])
|
||||||
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
->toContain("A dataset with the name [missing] does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
||||||
->toContain('FAILED')
|
->toContain('FAILED')
|
||||||
->toContain('Tests: 1 failed')
|
->toContain('Tests: 1 failed')
|
||||||
->and($result['code'])->not->toBe(0);
|
->and($result['code'])->not->toBe(0);
|
||||||
@@ -31,7 +31,7 @@ test('reports missing datasets as errors alongside passing tests', function () u
|
|||||||
$result = $run('tests/Fixtures/Suites/IssueWithPassing.php');
|
$result = $run('tests/Fixtures/Suites/IssueWithPassing.php');
|
||||||
|
|
||||||
expect($result['output'])
|
expect($result['output'])
|
||||||
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
->toContain("A dataset with the name [missing] does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
|
||||||
->toContain('1 passed')
|
->toContain('1 passed')
|
||||||
->toContain('1 failed')
|
->toContain('1 failed')
|
||||||
->and($result['code'])->not->toBe(0);
|
->and($result['code'])->not->toBe(0);
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ beforeEach(function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws exception if dataset does not exist', function (): void {
|
it('throws exception if dataset does not exist', function (): void {
|
||||||
expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
|
expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset with the name [first] does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws exception if dataset already exist', function (): void {
|
it('throws exception if dataset already exist', function (): void {
|
||||||
DatasetsRepository::set('second', [[]], __DIR__);
|
DatasetsRepository::set('second', [[]], __DIR__);
|
||||||
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name `second` already exists in scope ['.__DIR__.'].');
|
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name [second] already exists in scope ['.__DIR__.'].');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets closures', function (): void {
|
it('sets closures', function (): void {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// The folder name creates the namespace segment `2fa`, which starts with a number.
|
||||||
|
|
||||||
|
it('never runs')->assertTrue(true);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// The file name creates the class `list`, which is a PHP keyword.
|
||||||
|
|
||||||
|
it('never runs')->assertTrue(true);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// The file name creates the class `int`, which is a name PHP reserves.
|
||||||
|
|
||||||
|
it('never runs')->assertTrue(true);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// The file name creates the class `2fa`, which starts with a number.
|
||||||
|
|
||||||
|
it('never runs')->assertTrue(true);
|
||||||
@@ -15,7 +15,7 @@ it('does not allow to add the same test description twice', function (): void {
|
|||||||
$testSuite->tests->set($method);
|
$testSuite->tests->set($method);
|
||||||
})->throws(
|
})->throws(
|
||||||
TestAlreadyExist::class,
|
TestAlreadyExist::class,
|
||||||
sprintf('A test with the description `%s` already exists in the filename `%s`.', 'bar', 'foo'),
|
sprintf('A test with the description [%s] already exists in the filename [%s].', 'bar', 'foo'),
|
||||||
);
|
);
|
||||||
|
|
||||||
it('does not allow static closures', function (): void {
|
it('does not allow static closures', function (): void {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ test('a parallel test can extend another test with same name', function () use (
|
|||||||
|
|
||||||
test('parallel reports invalid datasets as failures', function () use ($run): void {
|
test('parallel reports invalid datasets as failures', function () use ($run): void {
|
||||||
expect($run('tests/Fixtures/Suites/ParallelInvalidDataset'))
|
expect($run('tests/Fixtures/Suites/ParallelInvalidDataset'))
|
||||||
->toContain("A dataset with the name `missing.dataset` does not exist. You can create it using `dataset('missing.dataset', ['a', 'b']);`.")
|
->toContain("A dataset with the name [missing.dataset] does not exist. You can create it using `dataset('missing.dataset', ['a', 'b']);`.")
|
||||||
->toContain('Tests: 1 failed, 1 passed (1 assertions)')
|
->toContain('Tests: 1 failed, 1 passed (1 assertions)')
|
||||||
->toContain('Parallel: 3 processes');
|
->toContain('Parallel: 3 processes');
|
||||||
})->skipOnWindows();
|
})->skipOnWindows();
|
||||||
|
|||||||
Reference in New Issue
Block a user