Introduce safe preg_split

Removes the need for extra assertions which makes the code
more readable, and reduces the overhead of an is_array check.
This commit is contained in:
Jaapio
2020-08-15 14:06:55 +02:00
parent 1ac416df3f
commit 690d9cd45f
14 changed files with 229 additions and 34 deletions
+2 -6
View File
@@ -14,13 +14,12 @@ declare(strict_types=1);
namespace phpDocumentor\Reflection\DocBlock; namespace phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use Webmozart\Assert\Assert; use phpDocumentor\Reflection\Utils;
use function count; use function count;
use function explode; use function explode;
use function implode; use function implode;
use function ltrim; use function ltrim;
use function min; use function min;
use function preg_split;
use function str_replace; use function str_replace;
use function strlen; use function strlen;
use function strpos; use function strpos;
@@ -98,7 +97,7 @@ class DescriptionFactory
return [$contents]; return [$contents];
} }
$parts = preg_split( return Utils::pregSplit(
'/\{ '/\{
# "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally. # "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally.
(?!@\}) (?!@\})
@@ -127,9 +126,6 @@ class DescriptionFactory
0, 0,
PREG_SPLIT_DELIM_CAPTURE PREG_SPLIT_DELIM_CAPTURE
); );
Assert::isArray($parts);
return $parts;
} }
/** /**
+2 -3
View File
@@ -18,8 +18,8 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\FqsenResolver; use phpDocumentor\Reflection\FqsenResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function preg_split;
/** /**
* Reflection class for a @covers tag in a Docblock. * Reflection class for a @covers tag in a Docblock.
@@ -51,8 +51,7 @@ final class Covers extends BaseTag implements Factory\StaticMethod
Assert::notNull($descriptionFactory); Assert::notNull($descriptionFactory);
Assert::notNull($resolver); Assert::notNull($resolver);
$parts = preg_split('/\s+/Su', $body, 2); $parts = Utils::pregSplit('/\s+/Su', $body, 2);
Assert::isArray($parts);
return new static( return new static(
$resolver->resolve($parts[0], $context), $resolver->resolve($parts[0], $context),
+2 -3
View File
@@ -16,8 +16,8 @@ namespace phpDocumentor\Reflection\DocBlock\Tags;
use phpDocumentor\Reflection\DocBlock\Description; use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\DescriptionFactory; use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function preg_split;
/** /**
* Reflection class for a @link tag in a Docblock. * Reflection class for a @link tag in a Docblock.
@@ -46,8 +46,7 @@ final class Link extends BaseTag implements Factory\StaticMethod
) : self { ) : self {
Assert::notNull($descriptionFactory); Assert::notNull($descriptionFactory);
$parts = preg_split('/\s+/Su', $body, 2); $parts = Utils::pregSplit('/\s+/Su', $body, 2);
Assert::isArray($parts);
$description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null; $description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null;
return new static($parts[0], $description); return new static($parts[0], $description);
+2 -3
View File
@@ -18,11 +18,11 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function array_shift; use function array_shift;
use function array_unshift; use function array_unshift;
use function implode; use function implode;
use function preg_split;
use function strpos; use function strpos;
use function substr; use function substr;
use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_DELIM_CAPTURE;
@@ -64,8 +64,7 @@ final class Param extends TagWithType implements Factory\StaticMethod
[$firstPart, $body] = self::extractTypeFromBody($body); [$firstPart, $body] = self::extractTypeFromBody($body);
$type = null; $type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
Assert::isArray($parts);
$variableName = ''; $variableName = '';
$isVariadic = false; $isVariadic = false;
+2 -3
View File
@@ -18,11 +18,11 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function array_shift; use function array_shift;
use function array_unshift; use function array_unshift;
use function implode; use function implode;
use function preg_split;
use function strpos; use function strpos;
use function substr; use function substr;
use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_DELIM_CAPTURE;
@@ -57,8 +57,7 @@ final class Property extends TagWithType implements Factory\StaticMethod
[$firstPart, $body] = self::extractTypeFromBody($body); [$firstPart, $body] = self::extractTypeFromBody($body);
$type = null; $type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
Assert::isArray($parts);
$variableName = ''; $variableName = '';
// if the first item that is encountered is not a variable; it is a type // if the first item that is encountered is not a variable; it is a type
+2 -3
View File
@@ -18,11 +18,11 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function array_shift; use function array_shift;
use function array_unshift; use function array_unshift;
use function implode; use function implode;
use function preg_split;
use function strpos; use function strpos;
use function substr; use function substr;
use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_DELIM_CAPTURE;
@@ -57,8 +57,7 @@ final class PropertyRead extends TagWithType implements Factory\StaticMethod
[$firstPart, $body] = self::extractTypeFromBody($body); [$firstPart, $body] = self::extractTypeFromBody($body);
$type = null; $type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
Assert::isArray($parts);
$variableName = ''; $variableName = '';
// if the first item that is encountered is not a variable; it is a type // if the first item that is encountered is not a variable; it is a type
+2 -3
View File
@@ -18,11 +18,11 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function array_shift; use function array_shift;
use function array_unshift; use function array_unshift;
use function implode; use function implode;
use function preg_split;
use function strpos; use function strpos;
use function substr; use function substr;
use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_DELIM_CAPTURE;
@@ -57,8 +57,7 @@ final class PropertyWrite extends TagWithType implements Factory\StaticMethod
[$firstPart, $body] = self::extractTypeFromBody($body); [$firstPart, $body] = self::extractTypeFromBody($body);
$type = null; $type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
Assert::isArray($parts);
$variableName = ''; $variableName = '';
// if the first item that is encountered is not a variable; it is a type // if the first item that is encountered is not a variable; it is a type
+2 -3
View File
@@ -20,9 +20,9 @@ use phpDocumentor\Reflection\DocBlock\Tags\Reference\Reference;
use phpDocumentor\Reflection\DocBlock\Tags\Reference\Url; use phpDocumentor\Reflection\DocBlock\Tags\Reference\Url;
use phpDocumentor\Reflection\FqsenResolver; use phpDocumentor\Reflection\FqsenResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function preg_match; use function preg_match;
use function preg_split;
/** /**
* Reflection class for an {@}see tag in a Docblock. * Reflection class for an {@}see tag in a Docblock.
@@ -53,8 +53,7 @@ final class See extends BaseTag implements Factory\StaticMethod
Assert::notNull($typeResolver); Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory); Assert::notNull($descriptionFactory);
$parts = preg_split('/\s+/Su', $body, 2); $parts = Utils::pregSplit('/\s+/Su', $body, 2);
Assert::isArray($parts);
$description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null; $description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null;
// https://tools.ietf.org/html/rfc2396#section-3 // https://tools.ietf.org/html/rfc2396#section-3
+2 -4
View File
@@ -18,8 +18,8 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\FqsenResolver; use phpDocumentor\Reflection\FqsenResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function preg_split;
/** /**
* Reflection class for a {@}uses tag in a Docblock. * Reflection class for a {@}uses tag in a Docblock.
@@ -50,9 +50,7 @@ final class Uses extends BaseTag implements Factory\StaticMethod
Assert::notNull($resolver); Assert::notNull($resolver);
Assert::notNull($descriptionFactory); Assert::notNull($descriptionFactory);
$parts = preg_split('/\s+/Su', $body, 2); $parts = Utils::pregSplit('/\s+/Su', $body, 2);
Assert::isArray($parts);
Assert::allString($parts);
return new static( return new static(
$resolver->resolve($parts[0], $context), $resolver->resolve($parts[0], $context),
+2 -3
View File
@@ -18,11 +18,11 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Types\Context as TypeContext;
use phpDocumentor\Reflection\Utils;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function array_shift; use function array_shift;
use function array_unshift; use function array_unshift;
use function implode; use function implode;
use function preg_split;
use function strpos; use function strpos;
use function substr; use function substr;
use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_DELIM_CAPTURE;
@@ -57,8 +57,7 @@ final class Var_ extends TagWithType implements Factory\StaticMethod
[$firstPart, $body] = self::extractTypeFromBody($body); [$firstPart, $body] = self::extractTypeFromBody($body);
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
Assert::isArray($parts);
$type = null; $type = null;
$variableName = ''; $variableName = '';
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\Exception;
use InvalidArgumentException;
use const PREG_BACKTRACK_LIMIT_ERROR;
use const PREG_BAD_UTF8_ERROR;
use const PREG_BAD_UTF8_OFFSET_ERROR;
use const PREG_INTERNAL_ERROR;
use const PREG_JIT_STACKLIMIT_ERROR;
use const PREG_NO_ERROR;
use const PREG_RECURSION_LIMIT_ERROR;
final class PcreException extends InvalidArgumentException
{
public static function createFromPhpError(int $errorCode) : self
{
switch ($errorCode) {
case PREG_BACKTRACK_LIMIT_ERROR:
return new self('Backtrack limit error');
case PREG_RECURSION_LIMIT_ERROR:
return new self('Recursion limit error');
case PREG_BAD_UTF8_ERROR:
return new self('Bad UTF8 error');
case PREG_BAD_UTF8_OFFSET_ERROR:
return new self('Bad UTF8 offset error');
case PREG_JIT_STACKLIMIT_ERROR:
return new self('Jit stacklimit error');
case PREG_NO_ERROR:
case PREG_INTERNAL_ERROR:
default:
}
return new self('Unknown Pcre error');
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection;
use phpDocumentor\Reflection\Exception\PcreException;
use function preg_last_error;
use function preg_split as php_preg_split;
abstract class Utils
{
/**
* Wrapper function for phps preg_split
*
* This function is inspired by {@link https://github.com/thecodingmachine/safe/blob/master/generated/pcre.php}. But
* since this library is all about performance we decided to strip everything we don't need. Reducing the amount
* of files that have to be loaded, ect.
*
* @param string $pattern The pattern to search for, as a string.
* @param string $subject The input string.
* @param int|null $limit If specified, then only substrings up to limit are returned with the
* rest of the string being placed in the last substring. A limit of -1 or 0 means "no limit".
* @param int $flags flags can be any combination of the following flags (combined with the | bitwise operator):
* *PREG_SPLIT_NO_EMPTY*
* If this flag is set, only non-empty pieces will be returned by preg_split().
* *PREG_SPLIT_DELIM_CAPTURE*
* If this flag is set, parenthesized expression in the delimiter pattern will be captured
* and returned as well.
* *PREG_SPLIT_OFFSET_CAPTURE*
* If this flag is set, for every occurring match the appendant string offset will also be returned.
* Note that this changes the return value in an array where every element is an array consisting of the
* matched string at offset 0 and its string offset into subject at offset 1.
*
* @return string[] Returns an array containing substrings of subject split along boundaries matched by pattern
*
* @throws PcreException
*/
public static function pregSplit(string $pattern, string $subject, ?int $limit = -1, int $flags = 0) : array
{
$parts = php_preg_split($pattern, $subject, $limit, $flags);
if ($parts === false) {
throw PcreException::createFromPhpError(preg_last_error());
}
return $parts;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\Exception;
use PHPUnit\Framework\TestCase;
use const PREG_BACKTRACK_LIMIT_ERROR;
use const PREG_BAD_UTF8_ERROR;
use const PREG_BAD_UTF8_OFFSET_ERROR;
use const PREG_INTERNAL_ERROR;
use const PREG_JIT_STACKLIMIT_ERROR;
use const PREG_NO_ERROR;
use const PREG_RECURSION_LIMIT_ERROR;
/**
* @coversDefaultClass \phpDocumentor\Reflection\Exception\PcreException
*/
final class PcreExceptionTest extends TestCase
{
/**
* @covers ::createFromPhpError
* @dataProvider errorCodeProvider
*/
public function testErrorConversion(int $errorCode, string $message) : void
{
$this->assertSame($message, PcreException::createFromPhpError($errorCode)->getMessage());
}
/**
* @return array<int, (string|int)[]>
*/
public function errorCodeProvider() : array
{
return [
[
PREG_BACKTRACK_LIMIT_ERROR,
'Backtrack limit error',
],
[
PREG_RECURSION_LIMIT_ERROR,
'Recursion limit error',
],
[
PREG_BAD_UTF8_ERROR,
'Bad UTF8 error',
],
[
PREG_BAD_UTF8_OFFSET_ERROR,
'Bad UTF8 offset error',
],
[
PREG_JIT_STACKLIMIT_ERROR,
'Jit stacklimit error',
],
[
PREG_NO_ERROR,
'Unknown Pcre error',
],
[
PREG_INTERNAL_ERROR,
'Unknown Pcre error',
],
];
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection;
use phpDocumentor\Reflection\Exception\PcreException;
use PHPUnit\Framework\TestCase;
use function set_error_handler;
use const E_WARNING;
final class PregSplitTest extends TestCase
{
/** @var callable|null */
private $errorHandler = null;
protected function tearDown() : void
{
if ($this->errorHandler === null) {
return;
}
set_error_handler($this->errorHandler, E_WARNING);
}
/**
* @covers \phpDocumentor\Reflection\Utils::pregSplit
*/
public function testSimplePregSplit() : void
{
$result = Utils::pregSplit('/\s/', 'word split');
$this->assertSame(['word', 'split'], $result);
}
/**
* @covers \phpDocumentor\Reflection\Utils::pregSplit
*/
public function testPregSplitThrowsOnError() : void
{
//We need to disable the error handler for phpunit... because we expect some errors here
$this->errorHandler = set_error_handler(static function () : void {
}, E_WARNING);
$this->expectException(PcreException::class);
Utils::pregSplit('~InvalidRegular)Expression~', 'some word');
}
}