Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: composer/pcre
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2.0.0
Choose a base ref
..
head repository: composer/pcre
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 3.3.2
Choose a head ref
Showing with 1,805 additions and 213 deletions.
  1. +1 −0 .gitattributes
  2. +7 −0 .github/dependabot.yml
  3. +21 −27 .github/workflows/continuous-integration.yml
  4. +21 −8 .github/workflows/lint.yml
  5. +11 −27 .github/workflows/phpstan.yml
  6. +35 −10 README.md
  7. +15 −7 composer.json
  8. +22 −0 extension.neon
  9. +110 −10 phpstan-baseline.neon
  10. +7 −2 phpstan.neon.dist
  11. +21 −12 phpunit.xml.dist
  12. +2 −2 src/MatchAllResult.php
  13. +46 −0 src/MatchAllStrictGroupsResult.php
  14. +1 −1 src/MatchAllWithOffsetsResult.php
  15. +1 −1 src/MatchResult.php
  16. +39 −0 src/MatchStrictGroupsResult.php
  17. +1 −1 src/MatchWithOffsetsResult.php
  18. +142 −0 src/PHPStan/InvalidRegexPatternRule.php
  19. +70 −0 src/PHPStan/PregMatchFlags.php
  20. +65 −0 src/PHPStan/PregMatchParameterOutTypeExtension.php
  21. +119 −0 src/PHPStan/PregMatchTypeSpecifyingExtension.php
  22. +91 −0 src/PHPStan/PregReplaceCallbackClosureTypeExtension.php
  23. +112 −0 src/PHPStan/UnsafeStrictGroupsCallRule.php
  24. +1 −6 src/PcreException.php
  25. +198 −51 src/Preg.php
  26. +75 −17 src/Regex.php
  27. +1 −2 src/ReplaceResult.php
  28. +20 −0 src/UnexpectedNullMatchException.php
  29. +2 −6 tests/BaseTestCase.php
  30. +69 −0 tests/PHPStanTests/InvalidRegexPatternRuleTest.php
  31. +51 −0 tests/PHPStanTests/TypeInferenceTest.php
  32. +61 −0 tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php
  33. +22 −0 tests/PHPStanTests/fixtures/invalid-patterns.php
  34. +139 −0 tests/PHPStanTests/nsrt/preg-match.php
  35. +107 −0 tests/PHPStanTests/nsrt/preg-replace-callback.php
  36. +16 −0 tests/PregTests/MatchAllTest.php
  37. +16 −0 tests/PregTests/MatchTest.php
  38. +51 −0 tests/PregTests/ReplaceCallbackTest.php
  39. +15 −0 tests/RegexTests/MatchTest.php
  40. +1 −1 tests/RegexTests/ReplaceCallbackTest.php
  41. +0 −22 tests/phpstan-locate-phpunit-autoloader.php
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
/.gitignore export-ignore
/.php_cs export-ignore
/phpstan.neon.dist export-ignore
/phpstan-baseline.neon export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
/CONTRIBUTING.md export-ignore
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2

updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
48 changes: 21 additions & 27 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
@@ -4,51 +4,45 @@ on:
- push
- pull_request

env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1"
permissions:
contents: read

jobs:
tests:
name: "CI"

runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}

strategy:
matrix:
php-version:
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
experimental: [false]
os: [ubuntu-latest]
include:
- php-version: "8.3"
os: windows-latest
experimental: false

steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
- uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
coverage: none

- name: Get composer cache directory
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"

- name: Cache dependencies
uses: actions/cache@v2
- uses: ramsey/composer-install@v3
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
dependency-versions: highest

- name: "Install latest dependencies"
- name: Run tests
run: |
# Remove PHPStan as it requires a newer PHP
composer remove phpstan/phpstan phpstan/phpstan-strict-rules --dev --no-update
composer update ${{ env.COMPOSER_FLAGS }}
- name: "Run tests"
run: "vendor/bin/simple-phpunit --verbose"
vendor/bin/phpunit
vendor/bin/phpunit --testsuite phpstan
29 changes: 21 additions & 8 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -4,6 +4,9 @@ on:
- push
- pull_request

permissions:
contents: read

jobs:
tests:
name: "Lint"
@@ -13,18 +16,28 @@ jobs:
strategy:
matrix:
php-version:
- "7.2"
- "8.1"
- "7.4"
- "nightly"

steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
- uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
coverage: none

- name: "Lint PHP files"
run: "find src/ -type f -name '*.php' -print0 | xargs -0 -L1 -P4 -- php -l -f"
run: |
hasErrors=0
for f in $(find src/ tests/ -type f -name '*.php' ! -path '*/vendor/*' ! -path '*/Fixtures/*')
do
{ error="$(php -derror_reporting=-1 -ddisplay_errors=1 -l -f $f 2>&1 1>&3 3>&-)"; } 3>&1;
if [ "$error" != "" ]; then
while IFS= read -r line; do echo "::error file=$f::$line"; done <<< "$error"
hasErrors=1
fi
done
if [ $hasErrors -eq 1 ]; then
exit 1
fi
38 changes: 11 additions & 27 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
@@ -4,9 +4,8 @@ on:
- push
- pull_request

env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
SYMFONY_PHPUNIT_VERSION: ""
permissions:
contents: read

jobs:
tests:
@@ -17,35 +16,20 @@ jobs:
strategy:
matrix:
php-version:
- "7.2"
- "8.1"
- "7.4"
- "8.3"

steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
- uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
coverage: none

- name: Get composer cache directory
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"

- name: Cache dependencies
uses: actions/cache@v2
- uses: ramsey/composer-install@v3
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-

- name: "Install latest dependencies"
run: "composer update ${{ env.COMPOSER_FLAGS }}"

- name: "Initialize PHPUnit sources"
run: "vendor/bin/simple-phpunit --filter NO_TEST_JUST_AUTOLOAD_THANKS"
dependency-versions: highest

- name: "Run PHPStan"
run: "composer phpstan"
- name: Run PHPStan
run: composer phpstan
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -6,11 +6,14 @@ PCRE wrapping library that offers type-safe `preg_*` replacements.
This library gives you a way to ensure `preg_*` functions do not fail silently, returning
unexpected `null`s that may not be handled.

As of 2.0 this library also enforces [`PREG_UNMATCHED_AS_NULL`](#preg_unmatched_as_null) usage for all matching functions, [read more below](#preg_unmatched_as_null) to understand the implications.
As of 3.0 this library enforces [`PREG_UNMATCHED_AS_NULL`](#preg_unmatched_as_null) usage
for all matching and replaceCallback functions, [read more below](#preg_unmatched_as_null)
to understand the implications.

It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it
simplifies and reduces the possible return values from all the `preg_*` functions which
are quite packed with edge cases.
are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a
[PHPStan extension](#phpstan-extension) for parsing regular expressions and giving you even better output types.

This library is a thin wrapper around `preg_*` functions with [some limitations](#restrictions--limitations).
If you are looking for a richer API to handle regular expressions have a look at
@@ -32,7 +35,9 @@ $ composer require composer/pcre
Requirements
------------

* PHP 5.3.2 is required but using the latest version of PHP is highly recommended.
* PHP 7.4.0 is required for 3.x versions
* PHP 7.2.0 is required for 2.x versions
* PHP 5.3.2 is required for 1.x versions


Basic usage
@@ -80,6 +85,23 @@ if (Preg::isMatch('{fo+}', $string, $matches)) // bool
if (Preg::isMatchAll('{fo+}', $string, $matches)) // bool
```

Finally the `Preg` class provides a few `*StrictGroups` method variants that ensure match groups
are always present and thus non-nullable, making it easier to write type-safe code:

```php
use Composer\Pcre\Preg;

// $matches is guaranteed to be an array of strings, if a subpattern does not match and produces a null it will throw
if (Preg::matchStrictGroups('{fo+}', $string, $matches))
if (Preg::matchAllStrictGroups('{fo+}', $string, $matches))
```

**Note:** This is generally safe to use as long as you do not have optional subpatterns (i.e. `(something)?`
or `(something)*` or branches with a `|` that result in some groups not being matched at all).
A subpattern that can match an empty string like `(.*)` is **not** optional, it will be present as an
empty string in the matches. A non-matching subpattern, even if optional like `(?:foo)?` will anyway not be present in
matches so it is also not a problem to use these with `*StrictGroups` methods.

If you would prefer a slightly more verbose usage, replacing by-ref arguments by result objects, you can use the `Regex` class:

```php
@@ -125,17 +147,13 @@ Due to type safety requirements a few restrictions are in place.
- `replace`, `replaceCallback` and `replaceCallbackArray` do not support an array `$subject`,
only simple strings.
- As of 2.0, the library always uses `PREG_UNMATCHED_AS_NULL` for matching, which offers [much
saner/more predictable results](#preg_unmatched_as_null). 3.x will also use the flag for
`replaceCallback` and `replaceCallbackArray`. This is currently not doable due to the PHP
version requirement and to keep things working the same across all PHP versions. If you use
the library on a PHP 7.4+ project however we highly recommend already passing
`PREG_UNMATCHED_AS_NULL` to `replaceCallback` and `replaceCallbackArray`.
saner/more predictable results](#preg_unmatched_as_null). As of 3.0 the flag is also set for
`replaceCallback` and `replaceCallbackArray`.

#### PREG_UNMATCHED_AS_NULL

As of 2.0, this library always uses PREG_UNMATCHED_AS_NULL for all `match*` and `isMatch*`
functions (unfortunately `preg_replace_callback[_array]` only support this from PHP 7.4
onwards so we cannot do the same there yet).
functions. As of 3.0 it is also done for `replaceCallback` and `replaceCallbackArray`.

This means your matches will always contain all matching groups, either as null if unmatched
or as string if it matched.
@@ -158,6 +176,13 @@ preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
| group 2 (any unmatched group preceding one that matched) is set to `''`. You cannot tell if it matched an empty string or did not match at all | group 2 is `null` when unmatched and a string if it matched, easy to check for |
| group 4 (any optional group without a matching one following) is missing altogether. So you have to check with `isset()`, but really you want `isset($m[4]) && $m[4] !== ''` for safety unless you are very careful to check that a non-optional group follows it | group 4 is always set, and null in this case as there was no match, easy to check for with `$m[4] !== null` |

PHPStan Extension
-----------------

To use the PHPStan extension if you do not use `phpstan/extension-installer` you can include `vendor/composer/pcre/extension.neon` in your PHPStan config.

The extension provides much better type information for $matches as well as regex validation where possible.

License
-------

22 changes: 15 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
@@ -17,12 +17,15 @@
}
],
"require": {
"php": "^7.2 || ^8.0"
"php": "^7.4 || ^8.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^5",
"phpstan/phpstan": "^1.3",
"phpstan/phpstan-strict-rules": "^1.1"
"phpunit/phpunit": "^8 || ^9",
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"autoload": {
"psr-4": {
@@ -36,11 +39,16 @@
},
"extra": {
"branch-alias": {
"dev-main": "2.x-dev"
"dev-main": "3.x-dev"
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"scripts": {
"test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit",
"phpstan": "phpstan analyse"
"test": "@php vendor/bin/phpunit",
"phpstan": "@php phpstan analyse"
}
}
22 changes: 22 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# composer/pcre PHPStan extensions
#
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
# in your phpstan config

services:
-
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
tags:
- phpstan.staticMethodParameterOutTypeExtension
-
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
-
class: Composer\Pcre\PHPStan\PregReplaceCallbackClosureTypeExtension
tags:
- phpstan.staticMethodParameterClosureTypeExtension

rules:
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule
- Composer\Pcre\PHPStan\InvalidRegexPatternRule
Loading