diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index d4dafb2aa0029..557eda9c29893 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,22 +1,25 @@
| Q | A
| ------------- | ---
-| Branch? | 7.4 for features / 6.4, 7.2, or 7.3 for bug fixes
+| Branch? | 7.4 for features / 6.4, 7.2, or 7.3 for bug fixes
| Bug fix? | yes/no
-| New feature? | yes/no
-| Deprecations? | yes/no
-| Issues | Fix #...
+| New feature? | yes/no
+| Deprecations? | yes/no
+| Issues | Fix #...
| License | MIT
diff --git a/.github/build-packages.php b/.github/build-packages.php
index d69a3c8198ec0..4793b8483d7ed 100644
--- a/.github/build-packages.php
+++ b/.github/build-packages.php
@@ -1,5 +1,15 @@
'__unset' !== $v);
+ }, []);
+
+ return $expandedVersions ?? [];
+}
+
if (3 > $_SERVER['argc']) {
echo "Usage: branch version dir1 dir2 ... dirN\n";
exit(1);
@@ -52,11 +62,13 @@
$packages[$package->name][$package->version] = $package;
- $versions = @file_get_contents('https://repo.packagist.org/p/'.$package->name.'.json') ?: sprintf('{"packages":{"%s":{"%s":%s}}}', $package->name, $package->version, file_get_contents($dir.'/composer.json'));
- $versions = json_decode($versions)->packages->{$package->name};
+ foreach (['.json', '~dev.json'] as $ext) {
+ $versions = @file_get_contents('https://repo.packagist.org/p2/'.$package->name.$ext) ?: '[]';
+ $versions = json_decode($versions, true)['packages'][$package->name] ?? [];
- foreach ($versions as $v => $package) {
- $packages[$package->name] += [$v => $package];
+ foreach (expandComposerMetadata($versions) as $p) {
+ $packages[$package->name] += [$p['version'] => $p];
+ }
}
}
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index bf81825134aed..6eff30a0a6e35 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -21,7 +21,7 @@ jobs:
name: Unit Tests
env:
- extensions: amqp,apcu,igbinary,intl,mbstring,memcached,redis,relay
+ extensions: amqp,apcu,brotli,igbinary,intl,mbstring,memcached,redis,relay,zstd
strategy:
matrix:
@@ -33,9 +33,6 @@ jobs:
mode: low-deps
- php: '8.3'
- php: '8.4'
- # brotli and zstd extensions are optional, when not present the commands will be used instead,
- # we must test both scenarios
- extensions: amqp,apcu,brotli,igbinary,intl,mbstring,memcached,redis,relay,zstd
- php: '8.5'
#mode: experimental
fail-fast: false
@@ -76,7 +73,7 @@ jobs:
([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json"
echo COLUMNS=120 >> $GITHUB_ENV
- echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV
+ echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration,transient" >> $GITHUB_ENV
echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.mode }}" != low-deps ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV
SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V)
@@ -101,7 +98,7 @@ jobs:
# Create local composer packages for each patched components and reference them in composer.json files when cross-testing components
if [[ ! "${{ matrix.mode }}" = *-deps ]]; then
- php .github/build-packages.php HEAD^ $SYMFONY_VERSION src/Symfony/Bridge/PhpUnit
+ php .github/build-packages.php HEAD^ $SYMFONY_VERSION src/Symfony/Bridge/PhpUnit
else
echo SYMFONY_DEPRECATIONS_HELPER=weak >> $GITHUB_ENV
cp composer.json composer.json.orig
@@ -217,7 +214,7 @@ jobs:
export SYMFONY_REQUIRE=">=$SYMFONY_VERSION"
git fetch --depth=2 origin $SYMFONY_VERSION
git checkout -m FETCH_HEAD
- PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true)
+ PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true)
if [[ $PATCHED_COMPONENTS ]]; then
echo "::group::install phpunit"
./phpunit install
@@ -233,6 +230,12 @@ jobs:
run: |
script -e -c './phpunit --group tty' /dev/null
+ - name: Run AssetMapper without ext-brotli nor ext-zstd
+ if: "! matrix.mode"
+ run: |
+ sudo rm /etc/php/*/cli/conf.d/*-{brotli,zstd}.ini
+ ./phpunit src/Symfony/Component/AssetMapper
+
- name: Run tests with SIGCHLD enabled PHP
if: "matrix.php == '8.2' && ! matrix.mode"
run: |
diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md
index 93c489ae487bd..d6d188669de42 100644
--- a/CHANGELOG-7.2.md
+++ b/CHANGELOG-7.2.md
@@ -7,6 +7,32 @@ in 7.2 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.2.0...v7.2.1
+* 7.2.7 (2025-05-29)
+
+ * bug #60549 [Translation] Add intl-icu fallback for MessageCatalogue metadata (pontus-mp)
+ * bug #60571 [ErrorHandler] Do not transform file to link if it does not exist (lyrixx)
+ * bug #60494 [Messenger] fix: Add argument as integer (overexpOG)
+ * bug #60524 [Notifier] Fix Clicksend transport (BafS)
+ * bug #60478 [Validator] add missing `$extensions` and `$extensionsMessage` to the `Image` constraint (xabbuh)
+ * bug #60484 [PhpUnitBridge] Clean up mocked features only when ``@group`` is present (HypeMC)
+ * bug #60490 [PhpUnitBridge] set path to the PHPUnit autoload file (xabbuh)
+ * bug #60423 [DependencyInjection] Make `DefinitionErrorExceptionPass` consider `IGNORE_ON_UNINITIALIZED_REFERENCE` and `RUNTIME_EXCEPTION_ON_INVALID_REFERENCE` the same (MatTheCat)
+ * bug #60439 [FrameworkBundle] Fix declaring field-attr tags in xml config files (nicolas-grekas)
+ * bug #60428 [DependencyInjection] Fix missing binding for ServiceCollectionInterface when declaring a service subscriber (nicolas-grekas)
+ * bug #60421 [VarExporter] Fixed lazy-loading ghost objects generation with property hooks (cheack)
+ * bug #60266 [Security] Exclude remember_me from default login authenticators (santysisi)
+ * bug #60400 [Config] Fix generated comment for multiline "info" (GromNaN)
+ * bug #60260 [Serializer] Prevent `Cannot traverse an already closed generator` error by materializing Traversable input (santysisi)
+ * bug #60292 [HttpFoundation] Encode path in `X-Accel-Redirect` header (Athorcis)
+ * bug #58643 [SecurityBundle] Use Composer `InstalledVersions` to check if flex is installed (andyexeter)
+ * bug #60275 [DoctrineBridge] Fix UniqueEntityValidator Stringable identifiers (GiuseppeArcuti, wkania)
+ * bug #60293 [Messenger] fix asking users to select an option if `--force` option is used in `messenger:failed:retry` command (W0rma)
+ * bug #60379 [Security] Avoid failing when PersistentRememberMeHandler handles a malformed cookie (Seldaek)
+ * bug #60373 [FrameworkBundle] Ensure `Email` class exists before using it (Kocal)
+ * bug #60365 [FrameworkBundle] ensure that all supported e-mail validation modes can be configured (xabbuh)
+ * bug #60350 [Security][LoginLink] Throw `InvalidLoginLinkException` on invalid parameters (davidszkiba)
+ * bug #60340 [String] fix EmojiTransliterator return type compatibility with PHP 8.5 (xabbuh)
+
* 7.2.6 (2025-05-02)
* bug #60288 [VarExporter] dump default value for property hooks if present (xabbuh)
diff --git a/CHANGELOG-7.3.md b/CHANGELOG-7.3.md
index bee0295a98485..da566f84844cb 100644
--- a/CHANGELOG-7.3.md
+++ b/CHANGELOG-7.3.md
@@ -7,6 +7,74 @@ in 7.3 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.3.0...v7.3.1
+* 7.3.1 (2025-06-28)
+
+ * bug #60044 [Console] Table counts wrong column width when using colspan and `setColumnMaxWidth()` (vladimir-vv)
+ * bug #60042 [Console] Table counts wrong number of padding symbols in `renderCell()` method when cell contain unicode variant selector (vladimir-vv)
+ * bug #60594 [Cache] Fix using a `ChainAdapter` as an adapter for a pool (IndraGunawan)
+ * bug #60483 [HttpKernel] Fix `#[MapUploadedFile]` handling for optional file uploads (santysisi)
+ * bug #60413 [Serializer] Fix collect_denormalization_errors flag in defaultContext (dmbrson)
+ * bug #60820 [TypeInfo] Fix handling `ConstFetchNode` (norkunas)
+ * bug #60908 [Uid] Improve entropy of the increment for UUIDv7 (nicolas-grekas)
+ * bug #60914 [Console] Fix command option mode (InputOption::VALUE_REQUIRED) (gharlan)
+ * bug #60919 [VarDumper] Avoid deprecated call in PgSqlCaster (vrana)
+ * bug #60909 [TypeInfo] use an EOL-agnostic approach to parse class uses (xabbuh)
+ * bug #60888 [Intl] Fix locale validator when canonicalize is true (rdavaillaud)
+ * bug #60885 [Notifier] Update fake SMS transports to use contracts event dispatcher (paulferrett)
+ * bug #60894 [FrameworkBundle] also deprecate the internal rate limiter factory alias (xabbuh)
+ * bug #60875 [HttpFoundation] Revert " Emit PHP warning when `Response::sendHeaders()` is called while output has already been sent" (nicolas-grekas)
+ * bug #60840 [Validator] Add missing HasNamedArguments to some constraints (jkgroupe)
+ * bug #60859 [TwigBundle] fix preload unlinked class `BinaryOperatorExpressionParser` (Grummfy)
+ * bug #60772 [Mailer] [Transport] Send clone of `RawMessage` instance in `RoundRobinTransport` (jnoordsij)
+ * bug #60842 [DependencyInjection] Fix generating adapters of functional interfaces (nicolas-grekas)
+ * bug #60809 [Serializer] Fix `TraceableSerializer` when called from a callable inside `array_map` (OrestisZag)
+ * bug #60831 [ObjectMapper] Fix parameter passed to class level transform (mttsch)
+ * bug #60511 [Serializer] Add support for discriminator map in property normalizer (ruudk)
+ * bug #60780 [FrameworkBundle] Fix argument not provided to `add_bus_name_stamp_middleware` (maxbaldanza)
+ * bug #60826 [DependencyInjection] Fix inlining when public services are involved (nicolas-grekas)
+ * bug #60806 [HttpClient] Limit curl's connection cache size (nicolas-grekas)
+ * bug #60699 [JsonPath] Improve compliance to the RFC test suite (alexandre-daubois)
+ * bug #60705 [FrameworkBundle] Fix allow `loose` as an email validation mode (rhel-eo)
+ * bug #60759 [Messenger] Fix float value for worker memory limit (ro0NL)
+ * bug #60785 [Security] Handle non-callable implementations of `FirewallListenerInterface` (MatTheCat)
+ * bug #60781 [DomCrawler] Allow selecting `button`s by their `value` (MatTheCat)
+ * bug #60775 [Validator] flip excluded properties with keys with Doctrine-style constraint config (xabbuh)
+ * bug #60774 [FrameworkBundle] Fixes getting a type error when the secret you are trying to reveal could not be decrypted (jack-worman)
+ * bug #60504 [JsonPath] Fix subexpression evaluation in filters (alexandre-daubois)
+ * bug #60779 Silence E_DEPRECATED and E_USER_DEPRECATED (nicolas-grekas)
+ * bug #60502 [HttpCache] Hit the backend only once after waiting for the cache lock (mpdude)
+ * bug #60771 [Runtime] fix compatibility with Symfony 7.4 (xabbuh)
+ * bug #60719 [JsonPath] Fix support for comma separated indices (alexandre-daubois)
+ * bug #59910 [Form] Keep submitted values when `keep_as_list` option of collection type is enabled (kells)
+ * bug #60638 [Form] Fix `keep_as_list` when data is not an array (MatTheCat)
+ * bug #60691 [DependencyInjection] Fix `ServiceLocatorTagPass` indexes handling (MatTheCat)
+ * bug #60676 [Form] Fix handling the empty string in NumberToLocalizedStringTransformer (gnat42)
+ * bug #60694 [Intl] Add missing currency (NOK) localization (en_NO) (llupa)
+ * bug #60681 [JsonPath] Better handling of unicode chars in expressions (alexandre-daubois)
+ * bug #60711 [Intl] Ensure data consistency between alpha and numeric codes (llupa)
+ * bug #60724 [VarDumper] Fix dumping LazyObjectState when using VarExporter v8 (nicolas-grekas)
+ * bug #60693 [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass (cquintana92)
+ * bug #60688 [Security] Keep roles when serializing tokens (nicolas-grekas)
+ * bug #60668 [JsonPath] Always use brackets notation with `JsonPath::key()` (alexandre-daubois)
+ * bug #60641 [TypeInfo] Fix type alias resolving (mtarld)
+ * bug #60564 [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass (cquintana92)
+ * bug #60632 [TypeInfo] Fix merging collection value types with union types (mtarld)
+ * bug #60645 [PhpUnitBridge] Skip bootstrap for PHPUnit >=10 (HypeMC)
+ * bug #60646 [FrameworkBundle] don't register `SchedulerTriggerNormalizer` without `symfony/serializer` (xabbuh)
+ * bug #60655 [TypeInfo] Handle `key-of` and `value-of` types (mtarld)
+ * bug #60640 [Mailer] use STARTTLS for SMTP with MailerSend (xabbuh)
+ * bug #60648 [Yaml] fix support for years outside of the 32b range on x86 arch on PHP 8.4 (nicolas-grekas)
+ * bug #60626 [Ldap] Fix `LdapUser::isEqualTo` (MatTheCat)
+ * bug #60625 [FrameworkBundle] set NamespacedPoolInterface alias to cache.app (IndraGunawan)
+ * bug #60607 [WebProfilerBundle] Fix toolbar with ajax requests not closing (HypeMC)
+ * bug #60606 [HttpKernel] Fix Symfony 7.3 end of maintenance date (axzx)
+ * bug #60616 skip interactive questions asked by Composer (xabbuh)
+ * bug #60617 [HttpKernel] pass log level instead of exception to resolve the logger (xabbuh)
+ * bug #60569 [HttpKernel] Do not superseed private cache-control when no-store is set (alexander-schranz)
+ * bug #60584 [DependencyInjection] Make `YamlDumper` quote resolved env vars if necessary (MatTheCat)
+ * bug #60588 [Notifier][Clicksend] Fix lack of recipient in case DSN does not have optional LIST_ID param (alifanau)
+ * bug #60547 [HttpFoundation] Fixed 'Via' header regex (thecaliskan)
+
* 7.3.0 (2025-05-29)
* bug #60549 [Translation] Add intl-icu fallback for MessageCatalogue metadata (pontus-mp)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ee2cb2a40889b..3e7f5ec2b6e78 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -30,9 +30,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Kris Wallsmith (kriswallsmith)
- Jakub Zalas (jakubzalas)
- Yonel Ceruto (yonelceruto)
+ - HypeMC (hypemc)
- Hugo Hamon (hhamon)
- Tobias Nyholm (tobias)
- - HypeMC (hypemc)
- Jérôme Tamarelle (gromnan)
- Antoine Lamirault (alamirault)
- Samuel ROZE (sroze)
@@ -96,8 +96,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Henrik Bjørnskov (henrikbjorn)
- Ruud Kamphuis (ruudk)
- David Buchmann (dbu)
- - Andrej Hudec (pulzarraider)
- Tomas Norkūnas (norkunas)
+ - Andrej Hudec (pulzarraider)
- Jáchym Toušek (enumag)
- Hubert Lenoir (hubert_lenoir)
- Christian Raue
@@ -160,12 +160,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Włodzimierz Gajda (gajdaw)
- Javier Spagnoletti (phansys)
- Adrien Brault (adrienbrault)
+ - Florent Morselli (spomky_)
+ - soyuka
- Florian Voutzinos (florianv)
- Teoh Han Hui (teohhanhui)
- Przemysław Bogusz (przemyslaw-bogusz)
- Colin Frei
- excelwebzone
- - Florent Morselli (spomky_)
- Paráda József (paradajozsef)
- Maximilian Beckers (maxbeckers)
- Baptiste Clavié (talus)
@@ -175,17 +176,16 @@ The Symfony Connect username in parenthesis allows to get more information
- Dāvis Zālītis (k0d3r1s)
- Gordon Franke (gimler)
- Malte Schlüter (maltemaltesich)
- - soyuka
- jeremyFreeAgent (jeremyfreeagent)
- Michael Babker (mbabker)
- Alexis Lefebvre
+ - Hugo Alliaume (kocal)
- Christopher Hertel (chertel)
- Joshua Thijssen
- Vasilij Dusko
- Daniel Wehner (dawehner)
- Robert Schönthal (digitalkaoz)
- Smaine Milianni (ismail1432)
- - Hugo Alliaume (kocal)
- François-Xavier de Guillebon (de-gui_f)
- Andreas Schempp (aschempp)
- noniagriconomie
@@ -255,6 +255,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Alessandro Lai (jean85)
- 77web
- Gocha Ossinkine (ossinkine)
+ - matlec
- Jesse Rushlow (geeshoe)
- Matthieu Ouellette-Vachon (maoueh)
- Michał Pipa (michal.pipa)
@@ -286,7 +287,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Clément JOBEILI (dator)
- Andreas Möller (localheinz)
- Marek Štípek (maryo)
- - matlec
- Daniel Espendiller
- Arnaud PETITPAS (apetitpa)
- Michael Käfer (michael_kaefer)
@@ -310,6 +310,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Patrick Landolt (scube)
- Karoly Gossler (connorhu)
- Timo Bakx (timobakx)
+ - Quentin Devos
- Giorgio Premi
- Alan Poulain (alanpoulain)
- Ruben Gonzalez (rubenrua)
@@ -337,6 +338,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Nikolay Labinskiy (e-moe)
- Martin Schuhfuß (usefulthink)
- apetitpa
+ - wkania
- Guilliam Xavier
- Pierre Minnieur (pminnieur)
- Dominique Bongiraud
@@ -377,6 +379,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Pascal Montoya
- Julien Brochet
- François Pluchino (francoispluchino)
+ - W0rma
- Tristan Darricau (tristandsensio)
- Jan Sorgalla (jsor)
- henrikbjorn
@@ -401,7 +404,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Zan Baldwin (zanbaldwin)
- Tim Goudriaan (codedmonkey)
- BoShurik
- - Quentin Devos
- Adam Prager (padam87)
- Benoît Burnichon (bburnichon)
- maxime.steinhausser
@@ -428,7 +430,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Uwe Jäger (uwej711)
- javaDeveloperKid
- Chris Smith (cs278)
- - W0rma
- Lynn van der Berg (kjarli)
- Michaël Perrin (michael.perrin)
- Eugene Leonovich (rybakit)
@@ -438,6 +439,7 @@ The Symfony Connect username in parenthesis allows to get more information
- GordonsLondon
- Ray
- Philipp Cordes (corphi)
+ - Fabien S (bafs)
- Chekote
- Thomas Adam
- Anderson Müller
@@ -471,6 +473,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Marcos Sánchez
- Emanuele Panzeri (thepanz)
- Zmey
+ - Santiago San Martin (santysisi)
- Kim Hemsø Rasmussen (kimhemsoe)
- Maximilian Reichel (phramz)
- Samaël Villette (samadu61)
@@ -498,6 +501,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Manuel Kießling (manuelkiessling)
- Alexey Kopytko (sanmai)
- Warxcell (warxcell)
+ - SiD (plbsid)
- Atsuhiro KUBO (iteman)
- rudy onfroy (ronfroy)
- Serkan Yildiz (srknyldz)
@@ -507,7 +511,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Gabor Toth (tgabi333)
- realmfoo
- Joppe De Cuyper (joppedc)
- - Fabien S (bafs)
- Simon Podlipsky (simpod)
- Thomas Tourlourat (armetiz)
- Andrey Esaulov (andremaha)
@@ -612,7 +615,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Alex (aik099)
- Kieran Brahney
- Fabien Villepinte
- - SiD (plbsid)
- Greg Thornton (xdissent)
- Alex Bowers
- Kev
@@ -638,6 +640,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Ivan Sarastov (isarastov)
- flack (flack)
- Shein Alexey
+ - Link1515
- Joe Lencioni
- Daniel Tschinder
- Diego Agulló (aeoris)
@@ -758,6 +761,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jérémy REYNAUD (babeuloula)
- Faizan Akram Dar (faizanakram)
- Arkadius Stefanski (arkadius)
+ - Andy Palmer (andyexeter)
- Jonas Flodén (flojon)
- AnneKir
- Tobias Weichart
@@ -781,6 +785,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Giso Stallenberg (gisostallenberg)
- Rob Bast
- Roberto Espinoza (respinoza)
+ - Steven RENAUX (steven_renaux)
- Marvin Feldmann (breyndotechse)
- Soufian EZ ZANTAR (soezz)
- Marek Zajac
@@ -867,7 +872,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Dariusz Ruminski
- Bahman Mehrdad (bahman)
- Romain Gautier (mykiwi)
- - Link1515
- Matthieu Bontemps
- Erik Trapman
- De Cock Xavier (xdecock)
@@ -1010,7 +1014,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Jonas Elfering
- Mihai Stancu
- Nahuel Cuesta (ncuesta)
- - Santiago San Martin
- Chris Boden (cboden)
- EStyles (insidestyles)
- Christophe Villeger (seragan)
@@ -1065,7 +1068,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Pierrick VIGNAND (pierrick)
- Alex Bogomazov (alebo)
- aaa2000 (aaa2000)
- - Andy Palmer (andyexeter)
- Andrew Neil Forster (krciga22)
- Stefan Warman (warmans)
- Tristan Maindron (tmaindron)
@@ -1865,6 +1867,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Philipp Fritsche
- Léon Gersen
- tarlepp
+ - Giuseppe Arcuti
- Dustin Wilson
- Benjamin Paap (benjaminpaap)
- Claus Due (namelesscoder)
@@ -1958,7 +1961,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Bruno MATEU
- Jeremy Bush
- Lucas Bäuerle
- - Steven RENAUX (steven_renaux)
- Laurens Laman
- Thomason, James
- Dario Savella
@@ -2195,6 +2197,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Tim Ward
- Adiel Cristo (arcristo)
- Christian Flach (cmfcmf)
+ - Dennis Jaschinski (d.jaschinski)
- Fabian Kropfhamer (fabiank)
- Jeffrey Cafferata (jcidnl)
- Junaid Farooq (junaidfarooq)
@@ -2264,6 +2267,7 @@ The Symfony Connect username in parenthesis allows to get more information
- wivaku
- Markus Reinhold
- Jingyu Wang
+ - es
- steveYeah
- Asrorbek (asrorbek)
- Samy D (dinduks)
@@ -2278,6 +2282,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Alan Scott
- Juanmi Rodriguez Cerón
- twifty
+ - David Szkiba
- Andy Raines
- François Poguet
- Anthony Ferrara
@@ -2296,6 +2301,7 @@ The Symfony Connect username in parenthesis allows to get more information
- xdavidwu
- Benjamin RICHARD
- Raphaël Droz
+ - Vladimir Pakhomchik
- pdommelen
- Eric Stern
- ShiraNai7
@@ -2710,6 +2716,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Marcel Siegert
- ryunosuke
- Bruno BOUTAREL
+ - Athorcis
- John Stevenson
- everyx
- Richard Heine
@@ -2767,6 +2774,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Abdouarrahmane FOUAD (fabdouarrahmane)
- Jakub Janata (janatjak)
- Jm Aribau (jmaribau)
+ - Maciej Paprocki (maciekpaprocki)
- Matthew Foster (mfoster)
- Paul Seiffert (seiffert)
- Vasily Khayrulin (sirian)
@@ -3114,6 +3122,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Darryl Hein (xmmedia)
- Vladimir Sadicov (xtech)
- Marcel Berteler
+ - Ruud Seberechts
- sdkawata
- Frederik Schmitt
- Peter van Dommelen
@@ -3151,6 +3160,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Pierre Rineau
- Florian Morello
- Maxim Lovchikov
+ - ivelin vasilev
- adenkejawen
- Florent SEVESTRE (aniki-taicho)
- Ari Pringle (apringle)
@@ -3327,6 +3337,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Kevin Verschaeve (keversc)
- Kevin Herrera (kherge)
- Kubicki Kamil (kubik)
+ - Lauris Binde (laurisb)
- Luis Ramón López López (lrlopez)
- Vladislav Nikolayev (luxemate)
- Martin Mandl (m2mtech)
@@ -3372,7 +3383,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Youpie
- Jason Stephens
- Korvin Szanto
- - wkania
- srsbiz
- Taylan Kasap
- Michael Orlitzky
@@ -3585,6 +3595,7 @@ The Symfony Connect username in parenthesis allows to get more information
- mieszko4
- Steve Preston
- ibasaw
+ - koyolgecen
- Wojciech Skorodecki
- Kevin Frantz
- Neophy7e
@@ -3614,6 +3625,7 @@ The Symfony Connect username in parenthesis allows to get more information
- satalaondrej
- Matthias Dötsch
- jonmldr
+ - Nowfel2501
- Yevgen Kovalienia
- Lebnik
- Shude
@@ -3635,6 +3647,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Egor Gorbachev
- Julian Krzefski
- Derek Stephen McLean
+ - PatrickRedStar
- Norman Soetbeer
- zorn
- Yuriy Potemkin
@@ -3744,6 +3757,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Brandon Kelly (brandonkelly)
- Choong Wei Tjeng (choonge)
- Bermon Clément (chou666)
+ - Chris Shennan (chrisshennan)
- Citia (citia)
- Kousuke Ebihara (co3k)
- Loïc Vernet (coil)
@@ -3906,6 +3920,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Romain
- Xavier REN
- Kevin Meijer
+ - Ignacio Alveal
- max
- Alexander Bauer (abauer)
- Ahmad Mayahi (ahmadmayahi)
diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md
index 5c279372b7626..5fa4d18677279 100644
--- a/UPGRADE-7.3.md
+++ b/UPGRADE-7.3.md
@@ -8,6 +8,37 @@ Read more about this in the [Symfony documentation](https://symfony.com/doc/7.3/
If you're upgrading from a version below 7.2, follow the [7.2 upgrade guide](UPGRADE-7.2.md) first.
+Table of Contents
+-----------------
+
+Bundles
+
+ * [FrameworkBundle](#FrameworkBundle)
+ * [SecurityBundle](#SecurityBundle)
+ * [WebProfilerBundle](#WebProfilerBundle)
+
+Bridges
+
+ * [DoctrineBridge](#DoctrineBridge)
+
+Components
+
+ * [AssetMapper](#AssetMapper)
+ * [Console](#Console)
+ * [DependencyInjection](#DependencyInjection)
+ * [HttpFoundation](#HttpFoundation)
+ * [Ldap](#Ldap)
+ * [OptionsResolver](#OptionsResolver)
+ * [PropertyInfo](#PropertyInfo)
+ * [Security](#Security)
+ * [Notifier](#Notifier)
+ * [Serializer](#Serializer)
+ * [TypeInfo](#TypeInfo)
+ * [Validator](#Validator)
+ * [VarDumper](#VarDumper)
+ * [VarExporter](#VarExporter)
+ * [Workflow](#Workflow)
+
AssetMapper
-----------
@@ -193,8 +224,8 @@ SecurityBundle
* Deprecate the `security.hide_user_not_found` config option in favor of `security.expose_security_errors`
- Notifier
- --------
+Notifier
+--------
* Deprecate the `Sms77` transport, use `SevenIo` instead
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6909669ee14a8..27418b4002971 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -54,7 +54,7 @@
./src/Symfony/Bridge/*/Tests
./src/Symfony/Component/*/Tests
./src/Symfony/Component/*/*/Tests
- ./src/Symfony/Contract/*/Tests
+ ./src/Symfony/Contracts/*/Tests
./src/Symfony/Bundle/*/Tests
./src/Symfony/Bundle/*/Resources
./src/Symfony/Component/*/Resources
diff --git a/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php b/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php
index 40472ff73ef40..d96416b287c65 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php
@@ -58,9 +58,11 @@ public static function createTestConfiguration(): Configuration
{
$config = ORMSetup::createConfiguration(true);
$config->setEntityNamespaces(['SymfonyTestsDoctrine' => 'Symfony\Bridge\Doctrine\Tests\Fixtures']);
- $config->setAutoGenerateProxyClasses(true);
- $config->setProxyDir(sys_get_temp_dir());
- $config->setProxyNamespace('SymfonyTests\Doctrine');
+ if (\PHP_VERSION_ID < 80400 || !method_exists($config, 'enableNativeLazyObjects')) {
+ $config->setAutoGenerateProxyClasses(true);
+ $config->setProxyDir(sys_get_temp_dir());
+ $config->setProxyNamespace('SymfonyTests\Doctrine');
+ }
$config->setMetadataDriverImpl(new AttributeDriver([__DIR__.'/../Tests/Fixtures' => 'Symfony\Bridge\Doctrine\Tests\Fixtures'], true));
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
$config->setLazyGhostObjectEnabled(true);
diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php
index 04817d9389049..2a5f337f2b0df 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php
@@ -46,7 +46,11 @@ private function createExtractor(): DoctrineExtractor
$config = ORMSetup::createConfiguration(true);
$config->setMetadataDriverImpl(new AttributeDriver([__DIR__.'/../Tests/Fixtures' => 'Symfony\Bridge\Doctrine\Tests\Fixtures'], true));
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
- $config->setLazyGhostObjectEnabled(true);
+ if (\PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) {
+ $config->enableNativeLazyObjects(true);
+ } else {
+ $config->setLazyGhostObjectEnabled(true);
+ }
$eventManager = new EventManager();
$entityManager = new EntityManager(DriverManager::getConnection(['driver' => 'pdo_sqlite'], $config, $eventManager), $config, $eventManager);
diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
index 18014bb180012..4d9f7667da5c2 100644
--- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
+++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
@@ -138,6 +138,7 @@
'COMPOSER' => 'composer.json',
'COMPOSER_VENDOR_DIR' => 'vendor',
'COMPOSER_BIN_DIR' => 'bin',
+ 'COMPOSER_NO_INTERACTION' => '1',
'SYMFONY_SIMPLE_PHPUNIT_BIN_DIR' => __DIR__,
];
@@ -234,10 +235,10 @@
@copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd');
chdir("$PHPUNIT_VERSION_DIR");
if ($SYMFONY_PHPUNIT_REMOVE) {
- $passthruOrFail("$COMPOSER remove --no-update --no-interaction ".$SYMFONY_PHPUNIT_REMOVE);
+ $passthruOrFail("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE);
}
if ($SYMFONY_PHPUNIT_REQUIRE) {
- $passthruOrFail("$COMPOSER require --no-update --no-interaction ".$SYMFONY_PHPUNIT_REQUIRE);
+ $passthruOrFail("$COMPOSER require --no-update ".$SYMFONY_PHPUNIT_REQUIRE);
}
if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) {
$passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\"");
diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php
index 5fddda14eb847..24d593406c87a 100644
--- a/src/Symfony/Bridge/PhpUnit/bootstrap.php
+++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php
@@ -14,7 +14,11 @@
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler;
// Detect if we need to serialize deprecations to a file.
-if (in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) {
+if (
+ // Skip if we're using PHPUnit >=10
+ !class_exists(PHPUnit\Metadata\Metadata::class)
+ && in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')
+) {
DeprecationErrorHandler::collectDeprecations($file);
return;
@@ -46,6 +50,10 @@
}
}
-if ('disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) {
+if (
+ // Skip if we're using PHPUnit >=10
+ !class_exists(PHPUnit\Metadata\Metadata::class, false)
+ && 'disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')
+) {
DeprecationErrorHandler::register(getenv('SYMFONY_DEPRECATIONS_HELPER'));
}
diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json
index 1283dfe33a9b0..de9101f796d73 100644
--- a/src/Symfony/Bridge/PhpUnit/composer.json
+++ b/src/Symfony/Bridge/PhpUnit/composer.json
@@ -2,7 +2,9 @@
"name": "symfony/phpunit-bridge",
"type": "symfony-bridge",
"description": "Provides utilities for PHPUnit, especially user deprecation notices management",
- "keywords": [],
+ "keywords": [
+ "testing"
+ ],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php
index 150186b1d37ba..c2110ee76f683 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php
@@ -61,6 +61,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!\array_key_exists($name, $secrets)) {
$io->error(\sprintf('The secret "%s" does not exist.', $name));
+ return self::INVALID;
+ } elseif (null === $secrets[$name]) {
+ $io->error(\sprintf('The secret "%s" could not be decrypted.', $name));
+
return self::INVALID;
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php
index 9cdfdae04cb37..a320130d5a6e7 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php
@@ -69,7 +69,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
- new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain'),
+ new InputOption('domain', null, InputOption::VALUE_REQUIRED, 'The messages domain'),
new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Display only missing messages'),
new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Display only unused messages'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Load messages from all registered bundles'),
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationExtractCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationExtractCommand.php
index d7967bbe8cc85..c8e61b61a64a0 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationExtractCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationExtractCommand.php
@@ -72,15 +72,15 @@ protected function configure(): void
->setDefinition([
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
- new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'),
+ new InputOption('prefix', null, InputOption::VALUE_REQUIRED, 'Override the default prefix', '__'),
new InputOption('no-fill', null, InputOption::VALUE_NONE, 'Extract translation keys without filling in values'),
- new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format', 'xlf12'),
new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'),
new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'),
- new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'),
- new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically'),
- new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
+ new InputOption('domain', null, InputOption::VALUE_REQUIRED, 'Specify the domain to extract'),
+ new InputOption('sort', null, InputOption::VALUE_REQUIRED, 'Return list of messages sorted alphabetically'),
+ new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The %command.name% command extracts translation strings from templates
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index f4e137f04b980..d042d44b5faa4 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -1096,7 +1096,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e
->validate()->castToArray()->end()
->end()
->scalarNode('translation_domain')->defaultValue('validators')->end()
- ->enumNode('email_validation_mode')->values((class_exists(Email::class) ? Email::VALIDATION_MODES : ['html5-allow-no-tld', 'html5', 'strict']) + ['loose'])->defaultValue('html5')->end()
+ ->enumNode('email_validation_mode')->values(array_merge(class_exists(Email::class) ? Email::VALIDATION_MODES : ['html5-allow-no-tld', 'html5', 'strict'], ['loose']))->defaultValue('html5')->end()
->arrayNode('mapping')
->addDefaultsIfNotSet()
->fixXmlConfig('path')
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 347f3ed653c87..d3cefbb28fbe1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -61,6 +61,7 @@
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
+use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
@@ -302,6 +303,10 @@ public function load(array $configs, ContainerBuilder $container): void
// Load Cache configuration first as it is used by other components
$loader->load('cache.php');
+ if (!interface_exists(NamespacedPoolInterface::class)) {
+ $container->removeAlias(NamespacedPoolInterface::class);
+ }
+
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
@@ -1769,10 +1774,6 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
throw new LogicException('Validation support cannot be enabled as the Validator component is not installed. Try running "composer require symfony/validator".');
}
- if (!isset($config['email_validation_mode'])) {
- $config['email_validation_mode'] = 'loose';
- }
-
$loader->load('validator.php');
$validatorBuilder = $container->getDefinition('validator.builder');
@@ -2293,7 +2294,7 @@ private function registerSchedulerConfiguration(ContainerBuilder $container, Php
}
// BC layer Scheduler < 7.3
- if (!class_exists(SchedulerTriggerNormalizer::class)) {
+ if (!ContainerBuilder::willBeAvailable('symfony/serializer', DenormalizerInterface::class, ['symfony/framework-bundle', 'symfony/scheduler']) || !class_exists(SchedulerTriggerNormalizer::class)) {
$container->removeDefinition('serializer.normalizer.scheduler_trigger');
}
}
@@ -2371,16 +2372,18 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
$defaultMiddleware['after'][0]['arguments'] = [$bus['default_middleware']['allow_no_senders']];
$defaultMiddleware['after'][1]['arguments'] = [$bus['default_middleware']['allow_no_handlers']];
- // argument to add_bus_name_stamp_middleware
- $defaultMiddleware['before'][0]['arguments'] = [$busId];
-
$middleware = array_merge($defaultMiddleware['before'], $middleware, $defaultMiddleware['after']);
}
- foreach ($middleware as $middlewareItem) {
+ foreach ($middleware as $key => $middlewareItem) {
if (!$validationEnabled && \in_array($middlewareItem['id'], ['validation', 'messenger.middleware.validation'], true)) {
throw new LogicException('The Validation middleware is only available when the Validator component is installed and enabled. Try running "composer require symfony/validator".');
}
+
+ // argument to add_bus_name_stamp_middleware
+ if ('add_bus_name_stamp_middleware' === $middlewareItem['id']) {
+ $middleware[$key]['arguments'] = [$busId];
+ }
}
if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class)) {
@@ -3299,7 +3302,12 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde
if (interface_exists(RateLimiterFactoryInterface::class)) {
$container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter');
- $factoryAlias->setDeprecated('symfony/dependency-injection', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.');
+ $factoryAlias->setDeprecated('symfony/framework-bundle', '7.3', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName()));
+ $internalAliasId = \sprintf('.%s $%s.limiter', RateLimiterFactory::class, $name);
+
+ if ($container->hasAlias($internalAliasId)) {
+ $container->getAlias($internalAliasId)->setDeprecated('symfony/framework-bundle', '7.3', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName()));
+ }
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php
index 3d96ba05994ca..ae9d426a498c6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php
@@ -28,6 +28,7 @@
use Symfony\Component\Cache\Messenger\EarlyExpirationHandler;
use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer;
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\NamespacedPoolInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
return static function (ContainerConfigurator $container) {
@@ -250,6 +251,8 @@
->alias(CacheInterface::class, 'cache.app')
+ ->alias(NamespacedPoolInterface::class, 'cache.app')
+
->alias(TagAwareCacheInterface::class, 'cache.app.taggable')
;
};
diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php
index 882ec78628839..788601d2e91ed 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php
@@ -31,6 +31,9 @@ abstract public function reveal(string $name): ?string;
abstract public function remove(string $name): bool;
+ /**
+ * @return array
+ */
abstract public function list(bool $reveal = false): array;
protected function validateName(string $name): void
diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php
index 15952611ac1a1..3fab5f4e28525 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php
@@ -89,13 +89,13 @@ public function list(bool $reveal = false): array
foreach ($_ENV as $k => $v) {
if ('' !== ($v ?? '') && preg_match('/^\w+$/D', $k)) {
- $secrets[$k] = $reveal ? $v : null;
+ $secrets[$k] = \is_string($v) && $reveal ? $v : null;
}
}
foreach ($_SERVER as $k => $v) {
if ('' !== ($v ?? '') && preg_match('/^\w+$/D', $k)) {
- $secrets[$k] = $reveal ? $v : null;
+ $secrets[$k] = \is_string($v) && $reveal ? $v : null;
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
index b2c2eb4d23089..87925f73c9b52 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
@@ -39,6 +39,14 @@ protected function tearDown(): void
static::$booted = false;
}
+ public static function tearDownAfterClass(): void
+ {
+ static::ensureKernelShutdown();
+ static::$class = null;
+ static::$kernel = null;
+ static::$booted = false;
+ }
+
/**
* @throws \RuntimeException
* @throws \LogicException
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php
index 94643db2c92c5..d77d303d5c88b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php
@@ -46,6 +46,19 @@ public function testInvalidName()
$this->assertStringContainsString('The secret "undefinedKey" does not exist.', trim($tester->getDisplay(true)));
}
+ public function testFailedDecrypt()
+ {
+ $vault = $this->createMock(AbstractVault::class);
+ $vault->method('list')->willReturn(['secretKey' => null]);
+
+ $command = new SecretsRevealCommand($vault);
+
+ $tester = new CommandTester($command);
+ $this->assertSame(Command::INVALID, $tester->execute(['name' => 'secretKey']));
+
+ $this->assertStringContainsString('The secret "secretKey" could not be decrypted.', trim($tester->getDisplay(true)));
+ }
+
/**
* @backupGlobals enabled
*/
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php
new file mode 100644
index 0000000000000..452594d452af8
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php
@@ -0,0 +1,25 @@
+loadFromExtension('framework', [
+ 'annotations' => false,
+ 'http_method_override' => false,
+ 'handle_all_throwables' => true,
+ 'php_errors' => ['log' => true],
+ 'lock' => false,
+ 'messenger' => [
+ 'default_bus' => 'messenger.bus.commands',
+ 'buses' => [
+ 'messenger.bus.commands' => [
+ 'default_middleware' => false,
+ 'middleware' => [
+ 'add_bus_name_stamp_middleware',
+ 'send_message',
+ 'handle_message',
+ ],
+ ],
+ 'messenger.bus.events' => [
+ 'default_middleware' => true,
+ ],
+ ],
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
new file mode 100644
index 0000000000000..5e0b178510a17
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
new file mode 100644
index 0000000000000..79f8d7c87420b
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
@@ -0,0 +1,18 @@
+framework:
+ annotations: false
+ http_method_override: false
+ handle_all_throwables: true
+ php_errors:
+ log: true
+ lock: false
+ messenger:
+ default_bus: messenger.bus.commands
+ buses:
+ messenger.bus.commands:
+ default_middleware: false
+ middleware:
+ - "add_bus_name_stamp_middleware"
+ - "send_message"
+ - "handle_message"
+ messenger.bus.events:
+ default_middleware: true
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
index 5ef658693d1a3..b5f5f1ef5dc95 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
@@ -1099,6 +1099,28 @@ public function testMessengerWithMultipleBusesWithoutDeduplicateMiddleware()
$this->assertSame('messenger.bus.commands', (string) $container->getAlias('messenger.default_bus'));
}
+ public function testMessengerWithAddBusNameStampMiddleware()
+ {
+ $container = $this->createContainerFromFile('messenger_bus_name_stamp');
+
+ $this->assertTrue($container->has('messenger.bus.commands'));
+ $this->assertEquals([
+ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']],
+ ['id' => 'send_message', 'arguments' => []],
+ ['id' => 'handle_message', 'arguments' => []],
+ ], $container->getParameter('messenger.bus.commands.middleware'));
+ $this->assertTrue($container->has('messenger.bus.events'));
+ $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0));
+ $this->assertEquals([
+ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']],
+ ['id' => 'reject_redelivered_message_middleware'],
+ ['id' => 'dispatch_after_current_bus'],
+ ['id' => 'failed_message_processing_middleware'],
+ ['id' => 'send_message', 'arguments' => [true]],
+ ['id' => 'handle_message', 'arguments' => [false]],
+ ], $container->getParameter('messenger.bus.events.middleware'));
+ }
+
public function testMessengerWithMultipleBusesWithDeduplicateMiddleware()
{
if (!class_exists(DeduplicateMiddleware::class)) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
index f69a53932711c..65826f6987702 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
@@ -456,6 +456,7 @@ public static function emailValidationModeProvider()
foreach (Email::VALIDATION_MODES as $mode) {
yield [$mode];
}
+ yield ['loose'];
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php
index 8d3f15ba61680..d21d4d113d2e6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php
@@ -53,7 +53,7 @@ public function testNoDebug()
public function testNoDumpedXML()
{
- static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true, 'debug.container.dump' => false]);
+ static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'no_dump.yml', 'debug' => true]);
$application = new Application(static::$kernel);
$application->setAutoExit(false);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/no_dump.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/no_dump.yml
new file mode 100644
index 0000000000000..a9c709e9a6425
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/no_dump.yml
@@ -0,0 +1,5 @@
+imports:
+ - { resource: config.yml }
+
+parameters:
+ debug.container.dump: false
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index f1888bd7a2928..1711964b3472f 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -321,7 +321,7 @@ private function createFirewalls(array $config, ContainerBuilder $container): vo
$authenticators[$name] = ServiceLocatorTagPass::register($container, $firewallAuthenticatorRefs);
}
$contextId = 'security.firewall.map.context.'.$name;
- $isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
+ $isLazy = !$firewall['stateless'] && $firewall['lazy'];
$context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
$context = $container->setDefinition($contextId, $context);
$context
@@ -683,7 +683,7 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
return $this->createMissingUserProvider($container, $id, $factoryKey);
}
- if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) {
+ if ('remember_me' === $factoryKey) {
return 'security.user_providers';
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
index 63648bd67510e..7263f4247959b 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php
@@ -12,6 +12,7 @@
namespace Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -23,7 +24,7 @@
class FirewallContext
{
/**
- * @param iterable $listeners
+ * @param iterable $listeners
*/
public function __construct(
private iterable $listeners,
@@ -39,7 +40,7 @@ public function getConfig(): ?FirewallConfig
}
/**
- * @return iterable
+ * @return iterable
*/
public function getListeners(): iterable
{
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
index 812ac1f666978..0105c71775903 100644
--- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
+++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
@@ -46,6 +46,7 @@
use Twig\Extension\OptimizerExtension;
use Twig\Extension\StagingExtension;
use Twig\ExtensionSet;
+use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;
@@ -65,6 +66,7 @@
->tag('container.preload', ['class' => EscaperExtension::class])
->tag('container.preload', ['class' => OptimizerExtension::class])
->tag('container.preload', ['class' => StagingExtension::class])
+ ->tag('container.preload', ['class' => BinaryOperatorExpressionParser::class])
->tag('container.preload', ['class' => ExtensionSet::class])
->tag('container.preload', ['class' => Template::class])
->tag('container.preload', ['class' => TemplateWrapper::class])
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php
index 46175d1d1f82e..09e022be922b0 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.php
@@ -16,7 +16,7 @@
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) {
if (__DIR__ === dirname(realpath($trace['args'][3]))) {
- trigger_deprecation('symfony/routing', '7.3', 'The "profiler.xml" routing configuration file is deprecated, import "profile.php" instead.');
+ trigger_deprecation('symfony/routing', '7.3', 'The "profiler.xml" routing configuration file is deprecated, import "profiler.php" instead.');
break;
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php
index 81b471d228c05..d0383ee8fbef9 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/wdt.php
@@ -16,7 +16,7 @@
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) {
if (__DIR__ === dirname(realpath($trace['args'][3]))) {
- trigger_deprecation('symfony/routing', '7.3', 'The "xdt.xml" routing configuration file is deprecated, import "xdt.php" instead.');
+ trigger_deprecation('symfony/routing', '7.3', 'The "wdt.xml" routing configuration file is deprecated, import "wdt.php" instead.');
break;
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig
index 91e6dc05e658c..5adfd27796acf 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig
@@ -144,7 +144,7 @@
var ajaxToolbarPanel = document.querySelector('.sf-toolbar-block-ajax');
if (requestStack.length) {
- ajaxToolbarPanel.style.display = 'block';
+ ajaxToolbarPanel.style.display = '';
} else {
ajaxToolbarPanel.style.display = 'none';
}
diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
index b4d77fd74a7a6..1a8576477c801 100644
--- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
+++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
@@ -55,9 +55,11 @@ public function process(ContainerBuilder $container): void
continue;
}
$class = $adapter->getClass();
+ $providers = $adapter->getArguments();
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$class = $class ?: $adapter->getClass();
+ $providers += $adapter->getArguments();
if ($t = $adapter->getTag('cache.pool')) {
$tags[0] += $t[0];
}
@@ -87,7 +89,7 @@ public function process(ContainerBuilder $container): void
if (ChainAdapter::class === $class) {
$adapters = [];
- foreach ($adapter->getArgument(0) as $provider => $adapter) {
+ foreach ($providers['index_0'] ?? $providers[0] as $provider => $adapter) {
if ($adapter instanceof ChildDefinition) {
$chainedPool = $adapter;
} else {
diff --git a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php
index cea761f5f99ac..e2cebc77f1015 100644
--- a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php
+++ b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php
@@ -120,9 +120,9 @@ public function testLateCollect()
$stats = $collector->getStatistics();
$this->assertGreaterThan(0, $stats[self::INSTANCE_NAME]['time']);
- $this->assertEquals($stats[self::INSTANCE_NAME]['hits'], 0, 'hits');
- $this->assertEquals($stats[self::INSTANCE_NAME]['misses'], 1, 'misses');
- $this->assertEquals($stats[self::INSTANCE_NAME]['calls'], 1, 'calls');
+ $this->assertEquals(0, $stats[self::INSTANCE_NAME]['hits'], 'hits');
+ $this->assertEquals(1, $stats[self::INSTANCE_NAME]['misses'], 'misses');
+ $this->assertEquals(1, $stats[self::INSTANCE_NAME]['calls'], 'calls');
$this->assertInstanceOf(Data::class, $collector->getCalls());
}
diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
index ef64d1932da8f..6527cceff47f7 100644
--- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
+++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
@@ -209,7 +209,8 @@ public function testChainAdapterPool()
$container->register('cache.adapter.apcu', ApcuAdapter::class)
->setArguments([null, 0, null])
->addTag('cache.pool');
- $container->register('cache.chain', ChainAdapter::class)
+ $container->register('cache.adapter.chain', ChainAdapter::class);
+ $container->setDefinition('cache.chain', new ChildDefinition('cache.adapter.chain'))
->addArgument(['cache.adapter.array', 'cache.adapter.apcu'])
->addTag('cache.pool');
$container->setDefinition('cache.app', new ChildDefinition('cache.chain'))
@@ -224,7 +225,7 @@ public function testChainAdapterPool()
$this->assertSame('cache.chain', $appCachePool->getParent());
$chainCachePool = $container->getDefinition('cache.chain');
- $this->assertNotInstanceOf(ChildDefinition::class, $chainCachePool);
+ $this->assertInstanceOf(ChildDefinition::class, $chainCachePool);
$this->assertCount(2, $chainCachePool->getArgument(0));
$this->assertInstanceOf(ChildDefinition::class, $chainCachePool->getArgument(0)[0]);
$this->assertSame('cache.adapter.array', $chainCachePool->getArgument(0)[0]->getParent());
diff --git a/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php b/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php
new file mode 100644
index 0000000000000..367f82f7bb2b6
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/Relay/BgsaveTrait.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits\Relay;
+
+if (version_compare(phpversion('relay'), '0.11', '>=')) {
+ /**
+ * @internal
+ */
+ trait BgsaveTrait
+ {
+ public function bgsave($arg = null): \Relay\Relay|bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
+ }
+ }
+} else {
+ /**
+ * @internal
+ */
+ trait BgsaveTrait
+ {
+ public function bgsave($schedule = false): \Relay\Relay|bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->bgsave(...\func_get_args());
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/RelayProxy.php b/src/Symfony/Component/Cache/Traits/RelayProxy.php
index e0ca8873a0182..b6d48dd543dba 100644
--- a/src/Symfony/Component/Cache/Traits/RelayProxy.php
+++ b/src/Symfony/Component/Cache/Traits/RelayProxy.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Traits;
+use Symfony\Component\Cache\Traits\Relay\BgsaveTrait;
use Symfony\Component\Cache\Traits\Relay\CopyTrait;
use Symfony\Component\Cache\Traits\Relay\GeosearchTrait;
use Symfony\Component\Cache\Traits\Relay\GetrangeTrait;
@@ -31,6 +32,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
*/
class RelayProxy extends \Relay\Relay implements ResetInterface, LazyObjectInterface
{
+ use BgsaveTrait;
use CopyTrait;
use GeosearchTrait;
use GetrangeTrait;
@@ -338,11 +340,6 @@ public function lcs($key1, $key2, $options = null): mixed
return $this->initializeLazyObject()->lcs(...\func_get_args());
}
- public function bgsave($schedule = false): \Relay\Relay|bool
- {
- return $this->initializeLazyObject()->bgsave(...\func_get_args());
- }
-
public function save(): \Relay\Relay|bool
{
return $this->initializeLazyObject()->save(...\func_get_args());
diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index b4539fa1eeb50..f0e0a303ee905 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -1275,7 +1275,7 @@ private function splitStringByWidth(string $string, int $width): array
foreach (preg_split('//u', $m[0]) as $char) {
// test if $char could be appended to current line
- if (mb_strwidth($line.$char, 'utf8') <= $width) {
+ if (Helper::width($line.$char) <= $width) {
$line .= $char;
continue;
}
diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php
index ddb2e93035432..bdd8d9e956787 100644
--- a/src/Symfony/Component/Console/Helper/Helper.php
+++ b/src/Symfony/Component/Console/Helper/Helper.php
@@ -42,7 +42,9 @@ public static function width(?string $string): int
$string ??= '';
if (preg_match('//u', $string)) {
- return (new UnicodeString($string))->width(false);
+ $string = preg_replace('/[\p{Cc}\x7F]++/u', '', $string, -1, $count);
+
+ return (new UnicodeString($string))->width(false) + $count;
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php
index 2811d58d4a01a..8c3d0a521ef23 100644
--- a/src/Symfony/Component/Console/Helper/Table.php
+++ b/src/Symfony/Component/Console/Helper/Table.php
@@ -561,10 +561,7 @@ private function renderCell(array $row, int $column, string $cellFormat): string
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
- if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
- $width += \strlen($cell) - mb_strwidth($cell, $encoding);
- }
-
+ $width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0");
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
@@ -629,8 +626,48 @@ private function buildTableRows(array $rows): TableRows
foreach ($rows[$rowKey] as $column => $cell) {
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
- if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
- $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
+ $minWrappedWidth = 0;
+ $widthApplied = [];
+ $lengthColumnBorder = $this->getColumnSeparatorWidth() + Helper::width($this->style->getCellRowContentFormat()) - 2;
+ for ($i = $column; $i < ($column + $colspan); ++$i) {
+ if (isset($this->columnMaxWidths[$i])) {
+ $minWrappedWidth += $this->columnMaxWidths[$i];
+ $widthApplied[] = ['type' => 'max', 'column' => $i];
+ } elseif (($this->columnWidths[$i] ?? 0) > 0 && $colspan > 1) {
+ $minWrappedWidth += $this->columnWidths[$i];
+ $widthApplied[] = ['type' => 'min', 'column' => $i];
+ }
+ }
+ if (1 === \count($widthApplied)) {
+ if ($colspan > 1) {
+ $minWrappedWidth *= $colspan; // previous logic
+ }
+ } elseif (\count($widthApplied) > 1) {
+ $minWrappedWidth += (\count($widthApplied) - 1) * $lengthColumnBorder;
+ }
+
+ $cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell));
+ if ($minWrappedWidth && $cellWidth > $minWrappedWidth) {
+ $cell = $formatter->formatAndWrap($cell, $minWrappedWidth);
+ }
+ // update minimal columnWidths for spanned columns
+ if ($colspan > 1 && $minWrappedWidth > 0) {
+ $columnsMinWidthProcessed = [];
+ $cellWidth = min($cellWidth, $minWrappedWidth);
+ foreach ($widthApplied as $item) {
+ if ('max' === $item['type'] && $cellWidth >= $this->columnMaxWidths[$item['column']]) {
+ $minWidthColumn = $this->columnMaxWidths[$item['column']];
+ $this->columnWidths[$item['column']] = $minWidthColumn;
+ $columnsMinWidthProcessed[$item['column']] = true;
+ $cellWidth -= $minWidthColumn + $lengthColumnBorder;
+ }
+ }
+ for ($i = $column; $i < ($column + $colspan); ++$i) {
+ if (isset($columnsMinWidthProcessed[$i])) {
+ continue;
+ }
+ $this->columnWidths[$i] = $cellWidth + $lengthColumnBorder;
+ }
}
if (!str_contains($cell ?? '', "\n")) {
continue;
diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php
index a50ede664f8ee..52ae233011a3a 100644
--- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php
+++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php
@@ -1308,9 +1308,9 @@ public static function renderSetTitle()
'footer',
'default',
<<<'TABLE'
-+---------------+---- Multiline
++---------------+--- Multiline
header
-here -+------------------+
+here +------------------+
| ISBN | Title | Author |
+---------------+--------------------------+------------------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
@@ -1590,17 +1590,17 @@ public function testWithColspanAndMaxWith()
$expected =
<<getOutputContent($output)
);
}
+
+ public function testGithubIssue60038WidthOfCellWithEmoji()
+ {
+ $table = (new Table($output = $this->getOutputStream()))
+ ->setHeaderTitle('Test Title')
+ ->setHeaders(['Title', 'Author'])
+ ->setRows([
+ ["🎭 💫 ☯"." Divine Comedy", "Dante Alighieri"],
+ // the snowflake (e2 9d 84 ef b8 8f) has a variant selector
+ ["👑 ❄️ 🗡"." Game of Thrones", "George R.R. Martin"],
+ // the snowflake in text style (e2 9d 84 ef b8 8e) has a variant selector
+ ["❄︎❄︎❄︎ snowflake in text style ❄︎❄︎❄︎", ""],
+ ["And a very long line to show difference in previous lines", ""],
+ ])
+ ;
+ $table->render();
+
+ $this->assertSame(<<getOutputContent($output)
+ );
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
index 7a677ebbd4e20..3e87186432efa 100644
--- a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
+++ b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php
@@ -40,22 +40,22 @@ public function __get(mixed $name): mixed
}
if (isset($this->initializer)) {
- $this->service = ($this->initializer)();
+ if (\is_string($service = ($this->initializer)())) {
+ $service = (new \ReflectionClass($service))->newInstanceWithoutConstructor();
+ }
+ $this->service = $service;
unset($this->initializer);
}
return $this->service;
}
- public static function getCode(string $initializer, array $callable, Definition $definition, ContainerBuilder $container, ?string $id): string
+ public static function getCode(string $initializer, array $callable, string $class, ContainerBuilder $container, ?string $id): string
{
$method = $callable[1];
- $asClosure = 'Closure' === ($definition->getClass() ?: 'Closure');
- if ($asClosure) {
+ if ($asClosure = 'Closure' === $class) {
$class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass();
- } else {
- $class = $definition->getClass();
}
$r = $container->getReflectionClass($class);
diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
index cc3306c739638..de751213acad5 100644
--- a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
+++ b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
@@ -20,8 +20,8 @@
class AsTaggedItem
{
/**
- * @param string|null $index The property or method to use to index the item in the locator
- * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the locator
+ * @param string|null $index The property or method to use to index the item in the iterator/locator
+ * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the iterator/locator
*/
public function __construct(
public ?string $index = null,
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
index de4acb258c3a9..52af43f606256 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php
@@ -69,6 +69,9 @@ public function process(ContainerBuilder $container): void
if (!$this->graph->hasNode($id)) {
continue;
}
+ if ($definition->isPublic()) {
+ $this->connectedIds[$id] = true;
+ }
foreach ($this->graph->getNode($id)->getOutEdges() as $edge) {
if (isset($notInlinedIds[$edge->getSourceNode()->getId()])) {
$this->currentId = $id;
@@ -188,17 +191,13 @@ private function isInlineableDefinition(string $id, Definition $definition): boo
return true;
}
- if ($definition->isPublic()) {
+ if ($definition->isPublic()
+ || $this->currentId === $id
+ || !$this->graph->hasNode($id)
+ ) {
return false;
}
- if (!$this->graph->hasNode($id)) {
- return true;
- }
-
- if ($this->currentId === $id) {
- return false;
- }
$this->connectedIds[$id] = true;
$srcIds = [];
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
index 4befef860a66e..8c6b5b582770d 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
@@ -88,8 +88,7 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam
if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $reflector) {
$defaultIndex = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
}
- $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null;
- $index = $index ?? $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId;
+ $index ??= $defaultIndex ??= $definition->getTag('container.decorator')[0]['id'] ?? $serviceId;
$services[] = [$priority, ++$i, $index, $serviceId, $class];
}
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
index 81c14ac5cc4d0..eedc0f484243c 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
@@ -54,17 +54,41 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
$value->setClass(ServiceLocator::class);
}
- $services = $value->getArguments()[0] ?? null;
+ $values = $value->getArguments()[0] ?? null;
+ $services = [];
- if ($services instanceof TaggedIteratorArgument) {
- $services = $this->findAndSortTaggedServices($services, $this->container);
- }
-
- if (!\is_array($services)) {
+ if ($values instanceof TaggedIteratorArgument) {
+ foreach ($this->findAndSortTaggedServices($values, $this->container) as $k => $v) {
+ $services[$k] = new ServiceClosureArgument($v);
+ }
+ } elseif (!\is_array($values)) {
throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId));
+ } else {
+ $i = 0;
+
+ foreach ($values as $k => $v) {
+ if ($v instanceof ServiceClosureArgument) {
+ $services[$k] = $v;
+ continue;
+ }
+
+ if ($i === $k) {
+ if ($v instanceof Reference) {
+ $k = (string) $v;
+ }
+ ++$i;
+ } elseif (\is_int($k)) {
+ $i = null;
+ }
+
+ $services[$k] = new ServiceClosureArgument($v);
+ }
+ if (\count($services) === $i) {
+ ksort($services);
+ }
}
- $value->setArgument(0, self::map($services));
+ $value->setArgument(0, $services);
$id = '.service_locator.'.ContainerBuilder::hash($value);
@@ -83,8 +107,12 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
public static function register(ContainerBuilder $container, array $map, ?string $callerId = null): Reference
{
+ foreach ($map as $k => $v) {
+ $map[$k] = new ServiceClosureArgument($v);
+ }
+
$locator = (new Definition(ServiceLocator::class))
- ->addArgument(self::map($map))
+ ->addArgument($map)
->addTag('container.service_locator');
if (null !== $callerId && $container->hasDefinition($callerId)) {
@@ -109,29 +137,4 @@ public static function register(ContainerBuilder $container, array $map, ?string
return new Reference($id);
}
-
- public static function map(array $services): array
- {
- $i = 0;
-
- foreach ($services as $k => $v) {
- if ($v instanceof ServiceClosureArgument) {
- continue;
- }
-
- if ($i === $k) {
- if ($v instanceof Reference) {
- unset($services[$k]);
- $k = (string) $v;
- }
- ++$i;
- } elseif (\is_int($k)) {
- $i = null;
- }
-
- $services[$k] = new ServiceClosureArgument($v);
- }
-
- return $services;
- }
}
diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
index 47202cf7d9e9a..38208124d3baf 100644
--- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
@@ -790,10 +790,11 @@ public function parameterCannotBeEmpty(string $name, string $message): void
* * The parameter bag is frozen;
* * Extension loading is disabled.
*
- * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved using the current
- * env vars or be replaced by uniquely identifiable placeholders.
- * Set to "true" when you want to use the current ContainerBuilder
- * directly, keep to "false" when the container is dumped instead.
+ * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved at build time using
+ * the current env var values (true), or be resolved at runtime based
+ * on the environment (false). In general, this should be set to "true"
+ * when you want to use the current ContainerBuilder directly, and to
+ * "false" when the container is dumped instead.
*/
public function compile(bool $resolveEnvPlaceholders = false): void
{
@@ -1108,14 +1109,15 @@ private function createService(Definition $definition, array &$inlineServices, b
}
if (\is_array($callable) && (
- $callable[0] instanceof Reference
+ 'Closure' !== $class
+ || $callable[0] instanceof Reference
|| $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
)) {
$initializer = function () use ($callable, &$inlineServices) {
return $this->doResolveServices($callable[0], $inlineServices);
};
- $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';');
+ $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $class, $this, $id).';');
$this->shareService($definition, $proxy, $id, $inlineServices);
return $proxy;
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
index ee7e519a0c8aa..9568ad26b349c 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
@@ -1184,13 +1184,13 @@ private function addNewInstance(Definition $definition, string $return = '', ?st
throw new RuntimeException(\sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
}
- if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && (
+ if (['...'] === $arguments && ('Closure' !== ($class = $definition->getClass() ?: 'Closure') || $definition->isLazy() && (
$callable[0] instanceof Reference
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
- )) {
+ ))) {
$initializer = 'fn () => '.$this->dumpValue($callable[0]);
- return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail;
+ return $return.LazyClosure::getCode($initializer, $callable, $class, $this->container, $id).$tail;
}
if ($callable[0] instanceof Reference
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
index ec115500bb0cf..d79e7b90408b2 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
@@ -50,18 +50,18 @@ public function dump(array $options = []): string
$this->dumper ??= new YmlDumper();
- return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices());
+ return $this->addParameters()."\n".$this->addServices();
}
private function addService(string $id, Definition $definition): string
{
- $code = " $id:\n";
+ $code = " {$this->dumper->dump($id)}:\n";
if ($class = $definition->getClass()) {
if (str_starts_with($class, '\\')) {
$class = substr($class, 1);
}
- $code .= \sprintf(" class: %s\n", $this->dumper->dump($class));
+ $code .= \sprintf(" class: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($class)));
}
if (!$definition->isPrivate()) {
@@ -87,7 +87,7 @@ private function addService(string $id, Definition $definition): string
}
if ($definition->getFile()) {
- $code .= \sprintf(" file: %s\n", $this->dumper->dump($definition->getFile()));
+ $code .= \sprintf(" file: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($definition->getFile())));
}
if ($definition->isSynthetic()) {
@@ -238,7 +238,7 @@ private function dumpCallable(mixed $callable): mixed
}
}
- return $callable;
+ return $this->container->resolveEnvPlaceholders($callable);
}
/**
@@ -299,7 +299,7 @@ private function dumpValue(mixed $value): mixed
if (\is_array($value)) {
$code = [];
foreach ($value as $k => $v) {
- $code[$k] = $this->dumpValue($v);
+ $code[$this->container->resolveEnvPlaceholders($k)] = $this->dumpValue($v);
}
return $code;
@@ -319,7 +319,7 @@ private function dumpValue(mixed $value): mixed
throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
}
- return $value;
+ return $this->container->resolveEnvPlaceholders($value);
}
private function getServiceCall(string $id, ?Reference $reference = null): string
@@ -359,7 +359,7 @@ private function prepareParameters(array $parameters, bool $escape = true): arra
$filtered[$key] = $value;
}
- return $escape ? $this->escape($filtered) : $filtered;
+ return $escape ? $this->container->resolveEnvPlaceholders($this->escape($filtered)) : $filtered;
}
private function escape(array $arguments): array
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php b/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
index 428227d19e2bc..e1b5c2a90f2ed 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Argument/LazyClosureTest.php
@@ -33,7 +33,7 @@ public function testThrowsWhenNotUsingInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create adapter for service "foo" because "Symfony\Component\DependencyInjection\Tests\Argument\LazyClosureTest" is not an interface.');
- LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(self::class), new ContainerBuilder(), 'foo');
+ LazyClosure::getCode('foo', [new \stdClass(), 'bar'], self::class, new ContainerBuilder(), 'foo');
}
public function testThrowsOnNonFunctionalInterface()
@@ -41,7 +41,7 @@ public function testThrowsOnNonFunctionalInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create adapter for service "foo" because interface "Symfony\Component\DependencyInjection\Tests\Argument\NonFunctionalInterface" doesn\'t have exactly one method.');
- LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(NonFunctionalInterface::class), new ContainerBuilder(), 'foo');
+ LazyClosure::getCode('foo', [new \stdClass(), 'bar'], NonFunctionalInterface::class, new ContainerBuilder(), 'foo');
}
public function testThrowsOnUnknownMethodInInterface()
@@ -49,7 +49,7 @@ public function testThrowsOnUnknownMethodInInterface()
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot create lazy closure for service "bar" because its corresponding callable is invalid.');
- LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], new Definition(\Closure::class), new ContainerBuilder(), 'bar');
+ LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], \Closure::class, new ContainerBuilder(), 'bar');
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
index 812b47c7a6f1f..9a93067756d50 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php
@@ -86,6 +86,26 @@ public function testProcessValue()
$this->assertSame(CustomDefinition::class, \get_class($locator('inlines.service')));
}
+ public function testServiceListIsOrdered()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('bar', CustomDefinition::class);
+ $container->register('baz', CustomDefinition::class);
+
+ $container->register('foo', ServiceLocator::class)
+ ->setArguments([[
+ new Reference('baz'),
+ new Reference('bar'),
+ ]])
+ ->addTag('container.service_locator')
+ ;
+
+ (new ServiceLocatorTagPass())->process($container);
+
+ $this->assertSame(['bar', 'baz'], array_keys($container->getDefinition('foo')->getArgument(0)));
+ }
+
public function testServiceWithKeyOverwritesPreviousInheritedKey()
{
$container = new ContainerBuilder();
@@ -170,6 +190,27 @@ public function testTaggedServices()
$this->assertSame(TestDefinition2::class, $locator('baz')::class);
}
+ public function testTaggedServicesKeysAreKept()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('bar', TestDefinition1::class)->addTag('test_tag', ['index' => 0]);
+ $container->register('baz', TestDefinition2::class)->addTag('test_tag', ['index' => 1]);
+
+ $container->register('foo', ServiceLocator::class)
+ ->setArguments([new TaggedIteratorArgument('test_tag', 'index', null, true)])
+ ->addTag('container.service_locator')
+ ;
+
+ (new ServiceLocatorTagPass())->process($container);
+
+ /** @var ServiceLocator $locator */
+ $locator = $container->get('foo');
+
+ $this->assertSame(TestDefinition1::class, $locator(0)::class);
+ $this->assertSame(TestDefinition2::class, $locator(1)::class);
+ }
+
public function testIndexedByServiceIdWithDecoration()
{
$container = new ContainerBuilder();
@@ -201,15 +242,33 @@ public function testIndexedByServiceIdWithDecoration()
static::assertInstanceOf(DecoratedService::class, $locator->get(Service::class));
}
- public function testDefinitionOrderIsTheSame()
+ public function testServicesKeysAreKept()
{
$container = new ContainerBuilder();
$container->register('service-1');
$container->register('service-2');
+ $container->register('service-3');
$locator = ServiceLocatorTagPass::register($container, [
- new Reference('service-2'),
new Reference('service-1'),
+ 'service-2' => new Reference('service-2'),
+ 'foo' => new Reference('service-3'),
+ ]);
+ $locator = $container->getDefinition($locator);
+ $factories = $locator->getArguments()[0];
+
+ static::assertSame([0, 'service-2', 'foo'], array_keys($factories));
+ }
+
+ public function testDefinitionOrderIsTheSame()
+ {
+ $container = new ContainerBuilder();
+ $container->register('service-1');
+ $container->register('service-2');
+
+ $locator = ServiceLocatorTagPass::register($container, [
+ 'service-2' => new Reference('service-2'),
+ 'service-1' => new Reference('service-1'),
]);
$locator = $container->getDefinition($locator);
$factories = $locator->getArguments()[0];
diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
index 774b1f88b66e7..5e08e47ab908c 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
@@ -50,6 +50,7 @@
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
+use Symfony\Component\DependencyInjection\Tests\Compiler\MyCallable;
use Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface;
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
@@ -532,6 +533,19 @@ public function testClosureProxy()
$this->assertInstanceOf(Foo::class, $container->get('closure_proxy')->theMethod());
}
+ public function testClosureProxyWithStaticMethod()
+ {
+ $container = new ContainerBuilder();
+ $container->register('closure_proxy', SingleMethodInterface::class)
+ ->setPublic('true')
+ ->setFactory(['Closure', 'fromCallable'])
+ ->setArguments([[MyCallable::class, 'theMethodImpl']]);
+ $container->compile();
+
+ $this->assertInstanceOf(SingleMethodInterface::class, $container->get('closure_proxy'));
+ $this->assertSame(124, $container->get('closure_proxy')->theMethod());
+ }
+
public function testCreateServiceClass()
{
$builder = new ContainerBuilder();
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
index f9ff3fff786a3..3a21d7aa9a9c5 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
@@ -215,6 +215,26 @@ public function testDumpNonScalarTags()
$this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_array_tags.yml'), $dumper->dump());
}
+ public function testDumpResolvedEnvPlaceholders()
+ {
+ $container = new ContainerBuilder();
+ $container->setParameter('%env(PARAMETER_NAME)%', '%env(PARAMETER_VALUE)%');
+ $container
+ ->register('service', '%env(SERVICE_CLASS)%')
+ ->setFile('%env(SERVICE_FILE)%')
+ ->addArgument('%env(SERVICE_ARGUMENT)%')
+ ->setProperty('%env(SERVICE_PROPERTY_NAME)%', '%env(SERVICE_PROPERTY_VALUE)%')
+ ->addMethodCall('%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%'])
+ ->setFactory('%env(SERVICE_FACTORY)%')
+ ->setConfigurator('%env(SERVICE_CONFIGURATOR)%')
+ ->setPublic(true)
+ ;
+ $container->compile();
+ $dumper = new YamlDumper($container);
+
+ $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/container_with_env_placeholders.yml'), $dumper->dump());
+ }
+
private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '')
{
$parser = new Parser();
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php
index 3d42a8c770952..09479fe55d2ae 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/StaticConstructorAutoconfigure.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
-use Symfony\Component\DependencyInjection\Attribute\Factory;
#[Autoconfigure(bind: ['$foo' => 'foo'], constructor: 'create')]
class StaticConstructorAutoconfigure
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
index d72d7b3aec63a..9e07d0283e396 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
@@ -462,6 +462,11 @@ class MyCallable
public function __invoke(): void
{
}
+
+ public static function theMethodImpl(): int
+ {
+ return 124;
+ }
}
class MyInlineService
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php
index 216dca434e489..ccd8d2e0bf63b 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php
@@ -50,6 +50,6 @@ public function getRemovedIds(): array
*/
protected static function getBarService($container)
{
- return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => (new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } });
+ return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } });
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml
new file mode 100644
index 0000000000000..46c91130faecd
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/container_with_env_placeholders.yml
@@ -0,0 +1,19 @@
+parameters:
+ '%env(PARAMETER_NAME)%': '%env(PARAMETER_VALUE)%'
+
+services:
+ service_container:
+ class: Symfony\Component\DependencyInjection\ContainerInterface
+ public: true
+ synthetic: true
+ service:
+ class: '%env(SERVICE_CLASS)%'
+ public: true
+ file: '%env(SERVICE_FILE)%'
+ arguments: ['%env(SERVICE_ARGUMENT)%']
+ properties: { '%env(SERVICE_PROPERTY_NAME)%': '%env(SERVICE_PROPERTY_VALUE)%' }
+ calls:
+ - ['%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%']]
+
+ factory: '%env(SERVICE_FACTORY)%'
+ configurator: '%env(SERVICE_CONFIGURATOR)%'
diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php
index e41875e268203..7550da83d4fdb 100644
--- a/src/Symfony/Component/DomCrawler/Crawler.php
+++ b/src/Symfony/Component/DomCrawler/Crawler.php
@@ -747,12 +747,12 @@ public function selectImage(string $value): static
}
/**
- * Selects a button by name or alt value for images.
+ * Selects a button by its text content, id, value, name or alt attribute.
*/
public function selectButton(string $value): static
{
return $this->filterRelativeXPath(
- \sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
+ \sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value))
);
}
diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
index 5cdbbbf45870d..53169efcab8e5 100644
--- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
+++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTestCase.php
@@ -452,10 +452,10 @@ public function testFilterXpathComplexQueries()
$this->assertCount(0, $crawler->filterXPath('/body'));
$this->assertCount(1, $crawler->filterXPath('./body'));
$this->assertCount(1, $crawler->filterXPath('.//body'));
- $this->assertCount(5, $crawler->filterXPath('.//input'));
+ $this->assertCount(6, $crawler->filterXPath('.//input'));
$this->assertCount(4, $crawler->filterXPath('//form')->filterXPath('//button | //input'));
$this->assertCount(1, $crawler->filterXPath('body'));
- $this->assertCount(6, $crawler->filterXPath('//button | //input'));
+ $this->assertCount(8, $crawler->filterXPath('//button | //input'));
$this->assertCount(1, $crawler->filterXPath('//body'));
$this->assertCount(1, $crawler->filterXPath('descendant-or-self::body'));
$this->assertCount(1, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('./div'), 'A child selection finds only the current div');
@@ -723,16 +723,23 @@ public function testSelectButton()
$this->assertNotSame($crawler, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler');
$this->assertInstanceOf(Crawler::class, $crawler->selectButton('FooValue'), '->selectButton() returns a new instance of a crawler');
- $this->assertEquals(1, $crawler->selectButton('FooValue')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('FooName')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('FooId')->count(), '->selectButton() selects buttons');
+ $this->assertCount(1, $crawler->selectButton('FooValue'), '->selectButton() selects type-submit inputs by value');
+ $this->assertCount(1, $crawler->selectButton('FooName'), '->selectButton() selects type-submit inputs by name');
+ $this->assertCount(1, $crawler->selectButton('FooId'), '->selectButton() selects type-submit inputs by id');
- $this->assertEquals(1, $crawler->selectButton('BarValue')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('BarName')->count(), '->selectButton() selects buttons');
- $this->assertEquals(1, $crawler->selectButton('BarId')->count(), '->selectButton() selects buttons');
+ $this->assertCount(1, $crawler->selectButton('BarValue'), '->selectButton() selects type-button inputs by value');
+ $this->assertCount(1, $crawler->selectButton('BarName'), '->selectButton() selects type-button inputs by name');
+ $this->assertCount(1, $crawler->selectButton('BarId'), '->selectButton() selects type-button inputs by id');
- $this->assertEquals(1, $crawler->selectButton('FooBarValue')->count(), '->selectButton() selects buttons with form attribute too');
- $this->assertEquals(1, $crawler->selectButton('FooBarName')->count(), '->selectButton() selects buttons with form attribute too');
+ $this->assertCount(1, $crawler->selectButton('ImageAlt'), '->selectButton() selects type-image inputs by alt');
+
+ $this->assertCount(1, $crawler->selectButton('ButtonValue'), '->selectButton() selects buttons by value');
+ $this->assertCount(1, $crawler->selectButton('ButtonName'), '->selectButton() selects buttons by name');
+ $this->assertCount(1, $crawler->selectButton('ButtonId'), '->selectButton() selects buttons by id');
+ $this->assertCount(1, $crawler->selectButton('ButtonText'), '->selectButton() selects buttons by text content');
+
+ $this->assertCount(1, $crawler->selectButton('FooBarValue'), '->selectButton() selects buttons with form attribute too');
+ $this->assertCount(1, $crawler->selectButton('FooBarName'), '->selectButton() selects buttons with form attribute too');
}
public function testSelectButtonWithSingleQuotesInNameAttribute()
@@ -1322,6 +1329,9 @@ public function createTestCrawler($uri = null)
+
+ ButtonText
+
One
Two
diff --git a/src/Symfony/Component/ErrorHandler/Debug.php b/src/Symfony/Component/ErrorHandler/Debug.php
index d54a38c4cac12..b090040d024b4 100644
--- a/src/Symfony/Component/ErrorHandler/Debug.php
+++ b/src/Symfony/Component/ErrorHandler/Debug.php
@@ -20,7 +20,7 @@ class Debug
{
public static function enable(): ErrorHandler
{
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', 0);
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
index e9c9cb9e55259..a0b3434a6b828 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
@@ -39,14 +39,14 @@ public function __construct(
/**
* Transforms a normalized format into a localized money string.
*
- * @param int|float|null $value Normalized number
+ * @param int|float|string|null $value Normalized number
*
* @throws TransformationFailedException if the given value is not numeric or
* if the value cannot be transformed
*/
public function transform(mixed $value): string
{
- if (null !== $value && 1 !== $this->divisor) {
+ if (null !== $value && '' !== $value && 1 !== $this->divisor) {
if (!is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
index 3020dd1483c28..ffcbc1feee6d7 100644
--- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
+++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
@@ -41,14 +41,14 @@ public function __construct(
/**
* Transforms a number type into localized number.
*
- * @param int|float|null $value Number value
+ * @param int|float|string|null $value Number value
*
* @throws TransformationFailedException if the given value is not numeric
* or if the value cannot be transformed
*/
public function transform(mixed $value): string
{
- if (null === $value) {
+ if (null === $value || '' === $value) {
return '';
}
diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
index 299f919373403..a7da65bdb60fa 100644
--- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
+++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
@@ -199,7 +199,13 @@ public function onSubmit(FormEvent $event): void
}
if ($this->keepAsList) {
- $formReindex = [];
+ $formReindex = $dataKeys = [];
+ foreach ($data as $key => $value) {
+ $dataKeys[] = $key;
+ }
+ foreach ($dataKeys as $key) {
+ unset($data[$key]);
+ }
foreach ($form as $name => $child) {
$formReindex[] = $child;
$form->remove($name);
@@ -207,9 +213,9 @@ public function onSubmit(FormEvent $event): void
foreach ($formReindex as $index => $child) {
$form->add($index, $this->type, array_replace([
'property_path' => '['.$index.']',
- ], $this->options));
+ ], $this->options, ['data' => $child->getData()]));
+ $data[$index] = $child->getData();
}
- $data = array_values($data);
}
$event->setData($data);
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
index e5733ad96abb5..689c6f0d4da32 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php
@@ -54,6 +54,13 @@ public function testTransformExpectsNumeric()
$transformer->transform('abcd');
}
+ public function testTransformEmptyString()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->assertSame('', $transformer->transform(''));
+ }
+
public function testTransformEmpty()
{
$transformer = new MoneyToLocalizedStringTransformer();
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
index 37448db51030a..c0344b9f232ea 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php
@@ -49,6 +49,7 @@ public static function provideTransformations()
{
return [
[null, '', 'de_AT'],
+ ['', '', 'de_AT'],
[1, '1', 'de_AT'],
[1.5, '1,5', 'de_AT'],
[1234.5, '1234,5', 'de_AT'],
diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
index 934460c8f98a4..390f6b04a60c5 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php
@@ -310,7 +310,7 @@ public function testOnSubmitDealsWithObjectBackedIteratorAggregate()
$this->assertArrayNotHasKey(2, $event->getData());
}
- public function testOnSubmitDealsWithArrayBackedIteratorAggregate()
+ public function testOnSubmitDealsWithDoctrineCollection()
{
$this->builder->add($this->getBuilder('1'));
@@ -323,6 +323,19 @@ public function testOnSubmitDealsWithArrayBackedIteratorAggregate()
$this->assertArrayNotHasKey(2, $event->getData());
}
+ public function testKeepAsListWorksWithTraversableArrayAccess()
+ {
+ $this->builder->add($this->getBuilder('1'));
+
+ $data = new \ArrayIterator([0 => 'first', 1 => 'second', 2 => 'third']);
+ $event = new FormEvent($this->builder->getForm(), $data);
+ $listener = new ResizeFormListener(TextType::class, keepAsList: true);
+ $listener->onSubmit($event);
+
+ $this->assertCount(1, $event->getData());
+ $this->assertArrayHasKey(0, $event->getData());
+ }
+
public function testOnSubmitDeleteEmptyNotCompoundEntriesIfAllowDelete()
{
$this->builder->setData(['0' => 'first', '1' => 'second']);
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
index 2dec87b5c712c..bd52831e28c3d 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
@@ -257,7 +257,7 @@ public function testCollectionTypeKeepAsListOptionTrue()
{
$formMetadata = new ClassMetadata(Form::class);
$authorMetadata = (new ClassMetadata(Author::class))
- ->addPropertyConstraint('firstName', new NotBlank());
+ ->addPropertyConstraint('firstName', new Length(1));
$organizationMetadata = (new ClassMetadata(Organization::class))
->addPropertyConstraint('authors', new Valid());
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
@@ -301,22 +301,22 @@ public function testCollectionTypeKeepAsListOptionTrue()
$form->submit([
'authors' => [
0 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'foobar', // Fires a Length Error
'lastName' => 'lastName1',
],
// key "1" could be missing if we add 4 blank form entries and then remove it.
2 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'barfoo', // Fires a Length Error
'lastName' => 'lastName3',
],
3 => [
- 'firstName' => '', // Fires a Not Blank Error
+ 'firstName' => 'barbaz', // Fires a Length Error
'lastName' => 'lastName3',
],
],
]);
- // Form does have 3 not blank errors
+ // Form does have 3 length errors
$errors = $form->getErrors(true);
$this->assertCount(3, $errors);
@@ -328,12 +328,15 @@ public function testCollectionTypeKeepAsListOptionTrue()
];
$this->assertTrue($form->get('authors')->has('0'));
+ $this->assertSame('foobar', $form->get('authors')->get('0')->getData()->firstName);
$this->assertContains('data.authors[0].firstName', $errorPaths);
$this->assertTrue($form->get('authors')->has('1'));
+ $this->assertSame('barfoo', $form->get('authors')->get('1')->getData()->firstName);
$this->assertContains('data.authors[1].firstName', $errorPaths);
$this->assertTrue($form->get('authors')->has('2'));
+ $this->assertSame('barbaz', $form->get('authors')->get('2')->getData()->firstName);
$this->assertContains('data.authors[2].firstName', $errorPaths);
$this->assertFalse($form->get('authors')->has('3'));
diff --git a/src/Symfony/Component/HttpClient/AmpHttpClient.php b/src/Symfony/Component/HttpClient/AmpHttpClient.php
index 4c73fbaf3db24..0bfa824a9a9a5 100644
--- a/src/Symfony/Component/HttpClient/AmpHttpClient.php
+++ b/src/Symfony/Component/HttpClient/AmpHttpClient.php
@@ -33,7 +33,7 @@
use Symfony\Contracts\Service\ResetInterface;
if (!interface_exists(DelegateHttpClient::class)) {
- throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".');
+ throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^5".');
}
if (\PHP_VERSION_ID < 80400 && is_subclass_of(Request::class, HttpMessage::class)) {
diff --git a/src/Symfony/Component/HttpClient/HttpClient.php b/src/Symfony/Component/HttpClient/HttpClient.php
index 3eb3665614fd7..27659358bce4c 100644
--- a/src/Symfony/Component/HttpClient/HttpClient.php
+++ b/src/Symfony/Component/HttpClient/HttpClient.php
@@ -62,7 +62,7 @@ public static function create(array $defaultOptions = [], int $maxHostConnection
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
- @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
+ @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^5" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
return new NativeHttpClient($defaultOptions, $maxHostConnections);
}
diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
index 05d2a7c22870e..8af4c755833bd 100644
--- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
+++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php
@@ -50,7 +50,7 @@ public function __construct(int $maxHostConnections, int $maxPendingPushes)
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS') && 0 < $maxHostConnections) {
- $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? 4294967295 : $maxHostConnections;
+ $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? min(50 * $maxHostConnections, 4294967295) : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
diff --git a/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
index d03693694a746..dd45668a837d4 100644
--- a/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/AmpHttpClientTest.php
@@ -19,6 +19,14 @@
*/
class AmpHttpClientTest extends HttpClientTestCase
{
+ /**
+ * @group transient
+ */
+ public function testNonBlockingStream()
+ {
+ parent::testNonBlockingStream();
+ }
+
protected function getHttpClient(string $testCase): HttpClientInterface
{
return new AmpHttpClient(['verify_peer' => false, 'verify_host' => false, 'timeout' => 5]);
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index 7ca008fd01f13..39e43f50b4fcd 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -31,7 +31,6 @@
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
- "amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
@@ -46,6 +45,7 @@
},
"conflict": {
"amphp/amp": "<2.5",
+ "amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php
index 9f421525dacd5..dba930a242672 100644
--- a/src/Symfony/Component/HttpFoundation/Request.php
+++ b/src/Symfony/Component/HttpFoundation/Request.php
@@ -1384,7 +1384,7 @@ public function isMethodCacheable(): bool
public function getProtocolVersion(): ?string
{
if ($this->isFromTrustedProxy()) {
- preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches);
+ preg_match('~^(HTTP/)?([1-9]\.[0-9])\b~', $this->headers->get('Via') ?? '', $matches);
if ($matches) {
return 'HTTP/'.$matches[2];
diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php
index 6766f2c77099e..638b5bf601347 100644
--- a/src/Symfony/Component/HttpFoundation/Response.php
+++ b/src/Symfony/Component/HttpFoundation/Response.php
@@ -317,11 +317,6 @@ public function sendHeaders(?int $statusCode = null): static
{
// headers have already been sent by the developer
if (headers_sent()) {
- if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
- $statusCode ??= $this->statusCode;
- header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
- }
-
return $this;
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
index 84c8629db039c..5cfb980a7b43b 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
@@ -2417,6 +2417,8 @@ public static function protocolVersionProvider()
'trusted with via and protocol name' => ['HTTP/2.0', true, 'HTTP/1.0 fred, HTTP/1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'],
'trusted with broken via' => ['HTTP/2.0', true, 'HTTP/1^0 foo', 'HTTP/2.0'],
'trusted with partially-broken via' => ['HTTP/2.0', true, '1.0 fred, foo', 'HTTP/1.0'],
+ 'trusted with simple via' => ['HTTP/2.0', true, 'HTTP/1.0', 'HTTP/1.0'],
+ 'trusted with only version via' => ['HTTP/2.0', true, '1.0', 'HTTP/1.0'],
];
}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
index 2b1c42084448d..0ca965db4b02b 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
@@ -232,6 +232,10 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null
{
- return $request->files->get($attribute->name ?? $argument->getName(), []);
+ if (!($files = $request->files->get($attribute->name ?? $argument->getName(), [])) && ($argument->isNullable() || $argument->hasDefaultValue())) {
+ return null;
+ }
+
+ return $files;
}
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php
index e913edf9e538a..436e031bbbcac 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php
@@ -165,7 +165,6 @@ public function onKernelResponse(ResponseEvent $event): void
}
if (true === $cache->noStore) {
- $response->setPrivate();
$response->headers->addCacheControlDirective('no-store');
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
index 18e8bff4413d8..2599b27de0c97 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
@@ -161,15 +161,15 @@ public static function getSubscribedEvents(): array
/**
* Logs an exception.
- *
+ *
* @param ?string $logChannel
*/
protected function logException(\Throwable $exception, string $message, ?string $logLevel = null, /* ?string $logChannel = null */): void
{
$logChannel = (3 < \func_num_args() ? \func_get_arg(3) : null) ?? $this->resolveLogChannel($exception);
-
+
$logLevel ??= $this->resolveLogLevel($exception);
-
+
if(!$logger = $this->getLogger($logChannel)) {
return;
}
@@ -218,7 +218,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
$attributes = [
'_controller' => $this->controller,
'exception' => $exception,
- 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($exception)),
+ 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($this->resolveLogChannel($exception))),
];
$request = $request->duplicate(null, null, $attributes);
$request->setMethod('GET');
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php b/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php
new file mode 100644
index 0000000000000..f13946ad71a68
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/HttpCache/CacheWasLockedException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\HttpCache;
+
+/**
+ * @internal
+ */
+class CacheWasLockedException extends \Exception
+{
+}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index 3ef1b8dcb821f..2b1be6a95a707 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -210,7 +210,13 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
$this->record($request, 'reload');
$response = $this->fetch($request, $catch);
} else {
- $response = $this->lookup($request, $catch);
+ $response = null;
+ do {
+ try {
+ $response = $this->lookup($request, $catch);
+ } catch (CacheWasLockedException) {
+ }
+ } while (null === $response);
}
$this->restoreResponseBody($request, $response);
@@ -560,15 +566,7 @@ protected function lock(Request $request, Response $entry): bool
// wait for the lock to be released
if ($this->waitForLock($request)) {
- // replace the current entry with the fresh one
- $new = $this->lookup($request);
- $entry->headers = $new->headers;
- $entry->setContent($new->getContent());
- $entry->setStatusCode($new->getStatusCode());
- $entry->setProtocolVersion($new->getProtocolVersion());
- foreach ($new->headers->getCookies() as $cookie) {
- $entry->headers->setCookie($cookie);
- }
+ throw new CacheWasLockedException(); // unwind back to handle(), try again
} else {
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
$entry->setStatusCode(503);
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index bfef40fac58ad..4829bfb7dedc7 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -73,14 +73,14 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '7.3.0';
- public const VERSION_ID = 70300;
+ public const VERSION = '7.3.1';
+ public const VERSION_ID = 70301;
public const MAJOR_VERSION = 7;
public const MINOR_VERSION = 3;
- public const RELEASE_VERSION = 0;
+ public const RELEASE_VERSION = 1;
public const EXTRA_VERSION = '';
- public const END_OF_MAINTENANCE = '05/2025';
+ public const END_OF_MAINTENANCE = '01/2026';
public const END_OF_LIFE = '01/2026';
public function __construct(
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php
index 11ab6f36a1474..91e28c864e102 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php
@@ -307,6 +307,66 @@ static function () {},
$resolver->onKernelControllerArguments($event);
}
+ /**
+ * @dataProvider provideContext
+ */
+ public function testShouldAllowEmptyWhenNullable(RequestPayloadValueResolver $resolver, Request $request)
+ {
+ $attribute = new MapUploadedFile();
+ $argument = new ArgumentMetadata(
+ 'qux',
+ UploadedFile::class,
+ false,
+ false,
+ null,
+ true,
+ [$attribute::class => $attribute]
+ );
+ /** @var HttpKernelInterface&MockObject $httpKernel */
+ $httpKernel = $this->createMock(HttpKernelInterface::class);
+ $event = new ControllerArgumentsEvent(
+ $httpKernel,
+ static function () {},
+ $resolver->resolve($request, $argument),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ );
+ $resolver->onKernelControllerArguments($event);
+ $data = $event->getArguments()[0];
+
+ $this->assertNull($data);
+ }
+
+ /**
+ * @dataProvider provideContext
+ */
+ public function testShouldAllowEmptyWhenHasDefaultValue(RequestPayloadValueResolver $resolver, Request $request)
+ {
+ $attribute = new MapUploadedFile();
+ $argument = new ArgumentMetadata(
+ 'qux',
+ UploadedFile::class,
+ false,
+ true,
+ 'default-value',
+ false,
+ [$attribute::class => $attribute]
+ );
+ /** @var HttpKernelInterface&MockObject $httpKernel */
+ $httpKernel = $this->createMock(HttpKernelInterface::class);
+ $event = new ControllerArgumentsEvent(
+ $httpKernel,
+ static function () {},
+ $resolver->resolve($request, $argument),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ );
+ $resolver->onKernelControllerArguments($event);
+ $data = $event->getArguments()[0];
+
+ $this->assertSame('default-value', $data);
+ }
+
public static function provideContext(): iterable
{
$resolver = new RequestPayloadValueResolver(
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php
index b185ea8994b1f..d2c8ed0db63d5 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php
@@ -102,18 +102,18 @@ public function testResponseIsPublicIfConfigurationIsPublicTrueNoStoreFalse()
$this->assertFalse($this->response->headers->hasCacheControlDirective('no-store'));
}
- public function testResponseIsPrivateIfConfigurationIsPublicTrueNoStoreTrue()
+ public function testResponseKeepPublicIfConfigurationIsPublicTrueNoStoreTrue()
{
$request = $this->createRequest(new Cache(public: true, noStore: true));
$this->listener->onKernelResponse($this->createEventMock($request, $this->response));
- $this->assertFalse($this->response->headers->hasCacheControlDirective('public'));
- $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
+ $this->assertTrue($this->response->headers->hasCacheControlDirective('public'));
+ $this->assertFalse($this->response->headers->hasCacheControlDirective('private'));
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-store'));
}
- public function testResponseIsPrivateNoStoreIfConfigurationIsNoStoreTrue()
+ public function testResponseKeepPrivateNoStoreIfConfigurationIsNoStoreTrue()
{
$request = $this->createRequest(new Cache(noStore: true));
@@ -124,14 +124,14 @@ public function testResponseIsPrivateNoStoreIfConfigurationIsNoStoreTrue()
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-store'));
}
- public function testResponseIsPrivateIfSharedMaxAgeSetAndNoStoreIsTrue()
+ public function testResponseIsPublicIfSharedMaxAgeSetAndNoStoreIsTrue()
{
$request = $this->createRequest(new Cache(smaxage: 1, noStore: true));
$this->listener->onKernelResponse($this->createEventMock($request, $this->response));
- $this->assertFalse($this->response->headers->hasCacheControlDirective('public'));
- $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
+ $this->assertTrue($this->response->headers->hasCacheControlDirective('public'));
+ $this->assertFalse($this->response->headers->hasCacheControlDirective('private'));
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-store'));
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
index e82e8fd81b481..240b201306d92 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
+use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel;
@@ -662,6 +663,7 @@ public function testDegradationWhenCacheLocked()
*/
sleep(10);
+ $this->store = $this->createStore(); // create another store instance that does not hold the current lock
$this->request('GET', '/');
$this->assertHttpKernelIsNotCalled();
$this->assertEquals(200, $this->response->getStatusCode());
@@ -680,6 +682,64 @@ public function testDegradationWhenCacheLocked()
$this->assertEquals('Old response', $this->response->getContent());
}
+ public function testHitBackendOnlyOnceWhenCacheWasLocked()
+ {
+ // Disable stale-while-revalidate, it circumvents waiting for the lock
+ $this->cacheConfig['stale_while_revalidate'] = 0;
+
+ $this->setNextResponses([
+ [
+ 'status' => 200,
+ 'body' => 'initial response',
+ 'headers' => [
+ 'Cache-Control' => 'public, no-cache',
+ 'Last-Modified' => 'some while ago',
+ ],
+ ],
+ [
+ 'status' => 304,
+ 'body' => '',
+ 'headers' => [
+ 'Cache-Control' => 'public, no-cache',
+ 'Last-Modified' => 'some while ago',
+ ],
+ ],
+ [
+ 'status' => 500,
+ 'body' => 'The backend should not be called twice during revalidation',
+ 'headers' => [],
+ ],
+ ]);
+
+ $this->request('GET', '/'); // warm the cache
+
+ // Use a store that simulates a cache entry being locked upon first attempt
+ $this->store = new class(sys_get_temp_dir() . '/http_cache') extends Store {
+ private bool $hasLock = false;
+
+ public function lock(Request $request): bool
+ {
+ $hasLock = $this->hasLock;
+ $this->hasLock = true;
+
+ return $hasLock;
+ }
+
+ public function isLocked(Request $request): bool
+ {
+ return false;
+ }
+ };
+
+ $this->request('GET', '/'); // hit the cache with simulated lock/concurrency block
+
+ $this->assertEquals(200, $this->response->getStatusCode());
+ $this->assertEquals('initial response', $this->response->getContent());
+
+ $traces = $this->cache->getTraces();
+ $this->assertSame(['stale', 'valid', 'store'], current($traces));
+ }
+
public function testHitsCachedResponseWithSMaxAgeDirective()
{
$time = \DateTimeImmutable::createFromFormat('U', time() - 5);
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
index 26a29f16b2b75..88f6bed56f4cf 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
@@ -30,7 +30,7 @@ abstract class HttpCacheTestCase extends TestCase
protected $responses;
protected $catch;
protected $esi;
- protected Store $store;
+ protected ?Store $store = null;
protected function setUp(): void
{
@@ -115,7 +115,9 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi =
$this->kernel->reset();
- $this->store = new Store(sys_get_temp_dir().'/http_cache');
+ if (! $this->store) {
+ $this->store = $this->createStore();
+ }
if (!isset($this->cacheConfig['debug'])) {
$this->cacheConfig['debug'] = true;
@@ -183,4 +185,9 @@ public static function clearDirectory($directory)
closedir($fp);
}
+
+ protected function createStore(): Store
+ {
+ return new Store(sys_get_temp_dir() . '/http_cache');
+ }
}
diff --git a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
index 745e074157974..5d484edacc1b7 100644
--- a/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
+++ b/src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
@@ -160,7 +160,7 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
$alpha3ToAlpha2 = array_flip($alpha2ToAlpha3);
asort($alpha3ToAlpha2);
- $alpha2ToNumeric = $this->generateAlpha2ToNumericMapping($metadataBundle);
+ $alpha2ToNumeric = $this->generateAlpha2ToNumericMapping(array_flip($this->regionCodes), $metadataBundle);
$numericToAlpha2 = [];
foreach ($alpha2ToNumeric as $alpha2 => $numeric) {
// Add underscore prefix to force keys with leading zeros to remain as string keys.
@@ -231,7 +231,7 @@ private function generateAlpha2ToAlpha3Mapping(array $countries, ArrayAccessible
return $alpha2ToAlpha3;
}
- private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $metadataBundle): array
+ private function generateAlpha2ToNumericMapping(array $countries, ArrayAccessibleResourceBundle $metadataBundle): array
{
$aliases = iterator_to_array($metadataBundle['alias']['territory']);
@@ -250,6 +250,10 @@ private function generateAlpha2ToNumericMapping(ArrayAccessibleResourceBundle $m
continue;
}
+ if (!isset($countries[$data['replacement']])) {
+ continue;
+ }
+
if ('deprecated' === $data['reason']) {
continue;
}
diff --git a/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php b/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php
new file mode 100644
index 0000000000000..dc28340678e53
--- /dev/null
+++ b/src/Symfony/Component/Intl/Resources/data/currencies/en_NO.php
@@ -0,0 +1,10 @@
+ [
+ 'NOK' => [
+ 'kr',
+ 'Norwegian Krone',
+ ],
+ ],
+];
diff --git a/src/Symfony/Component/Intl/Resources/data/regions/meta.php b/src/Symfony/Component/Intl/Resources/data/regions/meta.php
index 1c9f233273af7..e0a99ccb7f5a8 100644
--- a/src/Symfony/Component/Intl/Resources/data/regions/meta.php
+++ b/src/Symfony/Component/Intl/Resources/data/regions/meta.php
@@ -755,7 +755,6 @@
'ZWE' => 'ZW',
],
'Alpha2ToNumeric' => [
- 'AA' => '958',
'AD' => '020',
'AE' => '784',
'AF' => '004',
@@ -943,18 +942,6 @@
'PW' => '585',
'PY' => '600',
'QA' => '634',
- 'QM' => '959',
- 'QN' => '960',
- 'QP' => '962',
- 'QQ' => '963',
- 'QR' => '964',
- 'QS' => '965',
- 'QT' => '966',
- 'QV' => '968',
- 'QW' => '969',
- 'QX' => '970',
- 'QY' => '971',
- 'QZ' => '972',
'RE' => '638',
'RO' => '642',
'RS' => '688',
@@ -1012,29 +999,6 @@
'VU' => '548',
'WF' => '876',
'WS' => '882',
- 'XC' => '975',
- 'XD' => '976',
- 'XE' => '977',
- 'XF' => '978',
- 'XG' => '979',
- 'XH' => '980',
- 'XI' => '981',
- 'XJ' => '982',
- 'XL' => '984',
- 'XM' => '985',
- 'XN' => '986',
- 'XO' => '987',
- 'XP' => '988',
- 'XQ' => '989',
- 'XR' => '990',
- 'XS' => '991',
- 'XT' => '992',
- 'XU' => '993',
- 'XV' => '994',
- 'XW' => '995',
- 'XX' => '996',
- 'XY' => '997',
- 'XZ' => '998',
'YE' => '887',
'YT' => '175',
'ZA' => '710',
@@ -1042,7 +1006,6 @@
'ZW' => '716',
],
'NumericToAlpha2' => [
- '_958' => 'AA',
'_020' => 'AD',
'_784' => 'AE',
'_004' => 'AF',
@@ -1230,18 +1193,6 @@
'_585' => 'PW',
'_600' => 'PY',
'_634' => 'QA',
- '_959' => 'QM',
- '_960' => 'QN',
- '_962' => 'QP',
- '_963' => 'QQ',
- '_964' => 'QR',
- '_965' => 'QS',
- '_966' => 'QT',
- '_968' => 'QV',
- '_969' => 'QW',
- '_970' => 'QX',
- '_971' => 'QY',
- '_972' => 'QZ',
'_638' => 'RE',
'_642' => 'RO',
'_688' => 'RS',
@@ -1299,29 +1250,6 @@
'_548' => 'VU',
'_876' => 'WF',
'_882' => 'WS',
- '_975' => 'XC',
- '_976' => 'XD',
- '_977' => 'XE',
- '_978' => 'XF',
- '_979' => 'XG',
- '_980' => 'XH',
- '_981' => 'XI',
- '_982' => 'XJ',
- '_984' => 'XL',
- '_985' => 'XM',
- '_986' => 'XN',
- '_987' => 'XO',
- '_988' => 'XP',
- '_989' => 'XQ',
- '_990' => 'XR',
- '_991' => 'XS',
- '_992' => 'XT',
- '_993' => 'XU',
- '_994' => 'XV',
- '_995' => 'XW',
- '_996' => 'XX',
- '_997' => 'XY',
- '_998' => 'XZ',
'_887' => 'YE',
'_175' => 'YT',
'_710' => 'ZA',
diff --git a/src/Symfony/Component/Intl/Tests/CountriesTest.php b/src/Symfony/Component/Intl/Tests/CountriesTest.php
index 7b921036b2a00..01f0f76f2e40a 100644
--- a/src/Symfony/Component/Intl/Tests/CountriesTest.php
+++ b/src/Symfony/Component/Intl/Tests/CountriesTest.php
@@ -527,7 +527,6 @@ class CountriesTest extends ResourceBundleTestCase
];
private const ALPHA2_TO_NUMERIC = [
- 'AA' => '958',
'AD' => '020',
'AE' => '784',
'AF' => '004',
@@ -715,18 +714,6 @@ class CountriesTest extends ResourceBundleTestCase
'PW' => '585',
'PY' => '600',
'QA' => '634',
- 'QM' => '959',
- 'QN' => '960',
- 'QP' => '962',
- 'QQ' => '963',
- 'QR' => '964',
- 'QS' => '965',
- 'QT' => '966',
- 'QV' => '968',
- 'QW' => '969',
- 'QX' => '970',
- 'QY' => '971',
- 'QZ' => '972',
'RE' => '638',
'RO' => '642',
'RS' => '688',
@@ -784,29 +771,6 @@ class CountriesTest extends ResourceBundleTestCase
'VU' => '548',
'WF' => '876',
'WS' => '882',
- 'XC' => '975',
- 'XD' => '976',
- 'XE' => '977',
- 'XF' => '978',
- 'XG' => '979',
- 'XH' => '980',
- 'XI' => '981',
- 'XJ' => '982',
- 'XL' => '984',
- 'XM' => '985',
- 'XN' => '986',
- 'XO' => '987',
- 'XP' => '988',
- 'XQ' => '989',
- 'XR' => '990',
- 'XS' => '991',
- 'XT' => '992',
- 'XU' => '993',
- 'XV' => '994',
- 'XW' => '995',
- 'XX' => '996',
- 'XY' => '997',
- 'XZ' => '998',
'YE' => '887',
'YT' => '175',
'ZA' => '710',
@@ -814,6 +778,19 @@ class CountriesTest extends ResourceBundleTestCase
'ZW' => '716',
];
+ public function testAllGettersGenerateTheSameDataSetCount()
+ {
+ $alpha2Count = count(Countries::getCountryCodes());
+ $alpha3Count = count(Countries::getAlpha3Codes());
+ $numericCodesCount = count(Countries::getNumericCodes());
+ $namesCount = count(Countries::getNames());
+
+ // we base all on Name count since it is the first to be generated
+ $this->assertEquals($namesCount, $alpha2Count, 'Alpha 2 count does not match');
+ $this->assertEquals($namesCount, $alpha3Count, 'Alpha 3 count does not match');
+ $this->assertEquals($namesCount, $numericCodesCount, 'Numeric codes count does not match');
+ }
+
public function testGetCountryCodes()
{
$this->assertSame(self::COUNTRIES, Countries::getCountryCodes());
@@ -992,7 +969,7 @@ public function testGetNumericCode()
public function testNumericCodeExists()
{
$this->assertTrue(Countries::numericCodeExists('250'));
- $this->assertTrue(Countries::numericCodeExists('982'));
+ $this->assertTrue(Countries::numericCodeExists('008'));
$this->assertTrue(Countries::numericCodeExists('716'));
$this->assertTrue(Countries::numericCodeExists('036'));
$this->assertFalse(Countries::numericCodeExists('667'));
diff --git a/src/Symfony/Component/JsonPath/JsonCrawler.php b/src/Symfony/Component/JsonPath/JsonCrawler.php
index 75c61e14f79d7..0793a5c5d7b14 100644
--- a/src/Symfony/Component/JsonPath/JsonCrawler.php
+++ b/src/Symfony/Component/JsonPath/JsonCrawler.php
@@ -80,19 +80,7 @@ private function evaluate(JsonPath $query): array
throw new InvalidJsonStringInputException($e->getMessage(), $e);
}
- $current = [$data];
-
- foreach ($tokens as $token) {
- $next = [];
- foreach ($current as $value) {
- $result = $this->evaluateToken($token, $value);
- $next = array_merge($next, $result);
- }
-
- $current = $next;
- }
-
- return $current;
+ return $this->evaluateTokensOnDecodedData($tokens, $data);
} catch (InvalidArgumentException $e) {
throw $e;
} catch (\Throwable $e) {
@@ -100,6 +88,23 @@ private function evaluate(JsonPath $query): array
}
}
+ private function evaluateTokensOnDecodedData(array $tokens, array $data): array
+ {
+ $current = [$data];
+
+ foreach ($tokens as $token) {
+ $next = [];
+ foreach ($current as $value) {
+ $result = $this->evaluateToken($token, $value);
+ $next = array_merge($next, $result);
+ }
+
+ $current = $next;
+ }
+
+ return $current;
+ }
+
private function evaluateToken(JsonPathToken $token, mixed $value): array
{
return match ($token->type) {
@@ -128,7 +133,11 @@ private function evaluateBracket(string $expr, mixed $value): array
return [];
}
- if ('*' === $expr) {
+ if (str_contains($expr, ',') && (str_starts_with($trimmed = trim($expr), ',') || str_ends_with($trimmed, ','))) {
+ throw new JsonCrawlerException($expr, 'Expression cannot have leading or trailing commas');
+ }
+
+ if ('*' === $expr = JsonPathUtils::normalizeWhitespace($expr)) {
return array_values($value);
}
@@ -163,8 +172,7 @@ private function evaluateBracket(string $expr, mixed $value): array
return $result;
}
- // start, end and step
- if (preg_match('/^(-?\d*):(-?\d*)(?::(-?\d+))?$/', $expr, $matches)) {
+ if (preg_match('/^(-?\d*+)\s*+:\s*+(-?\d*+)(?:\s*+:\s*+(-?\d++))?$/', $expr, $matches)) {
if (!array_is_list($value)) {
return [];
}
@@ -212,25 +220,72 @@ private function evaluateBracket(string $expr, mixed $value): array
// filter expressions
if (preg_match('/^\?(.*)$/', $expr, $matches)) {
- $filterExpr = $matches[1];
-
- if (preg_match('/^(\w+)\s*\([^()]*\)\s*([<>=!]+.*)?$/', $filterExpr)) {
+ if (preg_match('/^(\w+)\s*\([^()]*\)\s*([<>=!]+.*)?$/', $filterExpr = trim($matches[1]))) {
$filterExpr = "($filterExpr)";
}
if (!str_starts_with($filterExpr, '(')) {
- throw new JsonCrawlerException($expr, 'Invalid filter expression');
+ $filterExpr = "($filterExpr)";
}
- // remove outrer filter parentheses
+ // remove outer filter parentheses
$innerExpr = substr(substr($filterExpr, 1), 0, -1);
return $this->evaluateFilter($innerExpr, $value);
}
- // quoted strings for object keys
+ // comma-separated values, e.g. `['key1', 'key2', 123]` or `[0, 1, 'key']`
+ if (str_contains($expr, ',')) {
+ $parts = JsonPathUtils::parseCommaSeparatedValues($expr);
+
+ $result = [];
+
+ foreach ($parts as $part) {
+ $part = trim($part);
+
+ if ('*' === $part) {
+ $result = array_merge($result, array_values($value));
+ } elseif (preg_match('/^(-?\d*+)\s*+:\s*+(-?\d*+)(?:\s*+:\s*+(-?\d++))?$/', $part, $matches)) {
+ // slice notation
+ $sliceResult = $this->evaluateBracket($part, $value);
+ $result = array_merge($result, $sliceResult);
+ } elseif (preg_match('/^([\'"])(.*)\1$/', $part, $matches)) {
+ $key = JsonPathUtils::unescapeString($matches[2], $matches[1]);
+
+ if (array_is_list($value)) {
+ // for arrays, find ALL objects that contain this key
+ foreach ($value as $item) {
+ if (\is_array($item) && \array_key_exists($key, $item)) {
+ $result[] = $item;
+ }
+ }
+ } elseif (\array_key_exists($key, $value)) { // for objects, get the value for this key
+ $result[] = $value[$key];
+ }
+ } elseif (preg_match('/^-?\d+$/', $part)) {
+ // numeric index
+ $index = (int) $part;
+ if ($index < 0) {
+ $index = \count($value) + $index;
+ }
+
+ if (array_is_list($value) && \array_key_exists($index, $value)) {
+ $result[] = $value[$index];
+ } else {
+ // numeric index on a hashmap
+ $keysIndices = array_keys($value);
+ if (isset($keysIndices[$index]) && isset($value[$keysIndices[$index]])) {
+ $result[] = $value[$keysIndices[$index]];
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
if (preg_match('/^([\'"])(.*)\1$/', $expr, $matches)) {
- $key = stripslashes($matches[2]);
+ $key = JsonPathUtils::unescapeString($matches[2], $matches[1]);
return \array_key_exists($key, $value) ? [$value[$key]] : [];
}
@@ -246,10 +301,6 @@ private function evaluateFilter(string $expr, mixed $value): array
$result = [];
foreach ($value as $item) {
- if (!\is_array($item)) {
- continue;
- }
-
if ($this->evaluateFilterExpression($expr, $item)) {
$result[] = $item;
}
@@ -258,9 +309,31 @@ private function evaluateFilter(string $expr, mixed $value): array
return $result;
}
- private function evaluateFilterExpression(string $expr, array $context): bool
+ private function evaluateFilterExpression(string $expr, mixed $context): bool
{
- $expr = trim($expr);
+ $expr = JsonPathUtils::normalizeWhitespace($expr);
+
+ // remove outer parentheses if they wrap the entire expression
+ if (str_starts_with($expr, '(') && str_ends_with($expr, ')')) {
+ $depth = 0;
+ $isWrapped = true;
+ $i = -1;
+ while (null !== $char = $expr[++$i] ?? null) {
+ if ('(' === $char) {
+ ++$depth;
+ } elseif (')' === $char && 0 === --$depth && isset($expr[$i + 1])) {
+ $isWrapped = false;
+ break;
+ }
+ }
+ if ($isWrapped) {
+ $expr = trim(substr($expr, 1, -1));
+ }
+ }
+
+ if (str_starts_with($expr, '!')) {
+ return !$this->evaluateFilterExpression(trim(substr($expr, 1)), $context);
+ }
if (str_contains($expr, '&&')) {
$parts = array_map('trim', explode('&&', $expr));
@@ -294,15 +367,17 @@ private function evaluateFilterExpression(string $expr, array $context): bool
}
}
- if (str_starts_with($expr, '@.')) {
- $path = substr($expr, 2);
+ if ('@' === $expr) {
+ return true;
+ }
- return \array_key_exists($path, $context);
+ if (str_starts_with($expr, '@.')) {
+ return (bool) ($this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.substr($expr, 1))), $context)[0] ?? false);
}
// function calls
- if (preg_match('/^(\w+)\((.*)\)$/', $expr, $matches)) {
- $functionName = $matches[1];
+ if (preg_match('/^(\w++)\s*+\((.*)\)$/', $expr, $matches)) {
+ $functionName = trim($matches[1]);
if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
throw new JsonCrawlerException($expr, \sprintf('invalid function "%s"', $functionName));
}
@@ -315,10 +390,21 @@ private function evaluateFilterExpression(string $expr, array $context): bool
return false;
}
- private function evaluateScalar(string $expr, array $context): mixed
+ private function evaluateScalar(string $expr, mixed $context): mixed
{
- if (is_numeric($expr)) {
- return str_contains($expr, '.') ? (float) $expr : (int) $expr;
+ $expr = JsonPathUtils::normalizeWhitespace($expr);
+
+ if (JsonPathUtils::isJsonNumber($expr)) {
+ return str_contains($expr, '.') || str_contains(strtolower($expr), 'e') ? (float) $expr : (int) $expr;
+ }
+
+ // only validate tokens that look like standalone numbers
+ if (preg_match('/^[\d+\-.eE]+$/', $expr) && preg_match('/\d/', $expr)) {
+ throw new JsonCrawlerException($expr, \sprintf('Invalid number format "%s"', $expr));
+ }
+
+ if ('@' === $expr) {
+ return $context;
}
if ('true' === $expr) {
@@ -335,20 +421,21 @@ private function evaluateScalar(string $expr, array $context): mixed
// string literals
if (preg_match('/^([\'"])(.*)\1$/', $expr, $matches)) {
- return $matches[2];
+ return JsonPathUtils::unescapeString($matches[2], $matches[1]);
}
// current node references
- if (str_starts_with($expr, '@.')) {
- $path = substr($expr, 2);
+ if (str_starts_with($expr, '@')) {
+ if (!\is_array($context)) {
+ return null;
+ }
- return $context[$path] ?? null;
+ return $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.substr($expr, 1))), $context)[0] ?? null;
}
// function calls
- if (preg_match('/^(\w+)\((.*)\)$/', $expr, $matches)) {
- $functionName = $matches[1];
- if (!isset(self::RFC9535_FUNCTIONS[$functionName])) {
+ if (preg_match('/^(\w++)\((.*)\)$/', $expr, $matches)) {
+ if (!isset(self::RFC9535_FUNCTIONS[$functionName = trim($matches[1])])) {
throw new JsonCrawlerException($expr, \sprintf('invalid function "%s"', $functionName));
}
@@ -358,14 +445,43 @@ private function evaluateScalar(string $expr, array $context): mixed
return null;
}
- private function evaluateFunction(string $name, string $args, array $context): mixed
+ private function evaluateFunction(string $name, string $args, mixed $context): mixed
{
- $args = array_map(
- fn ($arg) => $this->evaluateScalar(trim($arg), $context),
- explode(',', $args)
- );
+ $argList = [];
+ $nodelistSizes = [];
+ if ($args = trim($args)) {
+ $args = JsonPathUtils::parseCommaSeparatedValues($args);
+ foreach ($args as $arg) {
+ $arg = trim($arg);
+ if (str_starts_with($arg, '$')) { // special handling for absolute paths
+ $results = $this->evaluate(new JsonPath($arg));
+ $argList[] = $results[0] ?? null;
+ $nodelistSizes[] = \count($results);
+ } elseif (!str_starts_with($arg, '@')) { // special handling for @ to track nodelist size
+ $argList[] = $this->evaluateScalar($arg, $context);
+ $nodelistSizes[] = 1;
+ } elseif ('@' === $arg) {
+ $argList[] = $context;
+ $nodelistSizes[] = 1;
+ } elseif (!\is_array($context)) {
+ $argList[] = null;
+ $nodelistSizes[] = 0;
+ } elseif (str_starts_with($pathPart = substr($arg, 1), '[')) {
+ // handle bracket expressions like @['a','d']
+ $results = $this->evaluateBracket(substr($pathPart, 1, -1), $context);
+ $argList[] = $results;
+ $nodelistSizes[] = \count($results);
+ } else {
+ // handle dot notation like @.a
+ $results = $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.$pathPart)), $context);
+ $argList[] = $results[0] ?? null;
+ $nodelistSizes[] = \count($results);
+ }
+ }
+ }
- $value = $args[0] ?? null;
+ $value = $argList[0] ?? null;
+ $nodelistSize = $nodelistSizes[0] ?? 0;
return match ($name) {
'length' => match (true) {
@@ -373,16 +489,16 @@ private function evaluateFunction(string $name, string $args, array $context): m
\is_array($value) => \count($value),
default => 0,
},
- 'count' => \is_array($value) ? \count($value) : 0,
+ 'count' => $nodelistSize,
'match' => match (true) {
- \is_string($value) && \is_string($args[1] ?? null) => (bool) @preg_match(\sprintf('/^%s$/', $args[1]), $value),
+ \is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match(\sprintf('/^%s$/u', $this->transformJsonPathRegex($argList[1])), $value),
default => false,
},
'search' => match (true) {
- \is_string($value) && \is_string($args[1] ?? null) => (bool) @preg_match("/$args[1]/", $value),
+ \is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match("/{$this->transformJsonPathRegex($argList[1])}/u", $value),
default => false,
},
- 'value' => $value,
+ 'value' => 1 < $nodelistSize ? null : (1 === $nodelistSize ? (\is_array($value) ? ($value[0] ?? null) : $value) : $value),
default => null,
};
}
@@ -415,4 +531,52 @@ private function compare(mixed $left, mixed $right, string $operator): bool
default => false,
};
}
+
+ /**
+ * Transforms JSONPath regex patterns to comply with RFC 9535.
+ *
+ * The main issue is that '.' should not match \r or \n but should
+ * match Unicode line separators U+2028 and U+2029.
+ */
+ private function transformJsonPathRegex(string $pattern): string
+ {
+ $result = '';
+ $inCharClass = false;
+ $escaped = false;
+ $i = -1;
+
+ while (null !== $char = $pattern[++$i] ?? null) {
+ if ($escaped) {
+ $result .= $char;
+ $escaped = false;
+ continue;
+ }
+
+ if ('\\' === $char) {
+ $result .= $char;
+ $escaped = true;
+ continue;
+ }
+
+ if ('[' === $char && !$inCharClass) {
+ $inCharClass = true;
+ $result .= $char;
+ continue;
+ }
+
+ if (']' === $char && $inCharClass) {
+ $inCharClass = false;
+ $result .= $char;
+ continue;
+ }
+
+ if ('.' === $char && !$inCharClass) {
+ $result .= '(?:[^\r\n]|\x{2028}|\x{2029})';
+ } else {
+ $result .= $char;
+ }
+ }
+
+ return $result;
+ }
}
diff --git a/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php b/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php
index 3e8a222f0ba8e..4859c2bde076b 100644
--- a/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php
+++ b/src/Symfony/Component/JsonPath/JsonCrawlerInterface.php
@@ -25,7 +25,7 @@ interface JsonCrawlerInterface
* @return list
*
* @throws InvalidArgumentException When the JSON string provided to the crawler cannot be decoded
- * @throws JsonCrawlerException When a syntax error occurs in the provided JSON path
+ * @throws JsonCrawlerException When a syntax error occurs in the provided JSON path
*/
public function find(string|JsonPath $query): array;
}
diff --git a/src/Symfony/Component/JsonPath/JsonPath.php b/src/Symfony/Component/JsonPath/JsonPath.php
index 1009369b0a56d..e36fc9ffd2ef1 100644
--- a/src/Symfony/Component/JsonPath/JsonPath.php
+++ b/src/Symfony/Component/JsonPath/JsonPath.php
@@ -30,7 +30,9 @@ public function __construct(
public function key(string $key): static
{
- return new self($this->path.(str_ends_with($this->path, '..') ? '' : '.').$key);
+ $escaped = $this->escapeKey($key);
+
+ return new self($this->path.'["'.$escaped.'"]');
}
public function index(int $index): static
@@ -80,4 +82,25 @@ public function __toString(): string
{
return $this->path;
}
+
+ private function escapeKey(string $key): string
+ {
+ $key = strtr($key, [
+ '\\' => '\\\\',
+ '"' => '\\"',
+ "\n" => '\\n',
+ "\r" => '\\r',
+ "\t" => '\\t',
+ "\b" => '\\b',
+ "\f" => '\\f',
+ ]);
+
+ for ($i = 0; $i <= 31; ++$i) {
+ if ($i < 8 || $i > 13) {
+ $key = str_replace(\chr($i), \sprintf('\\u%04x', $i), $key);
+ }
+ }
+
+ return $key;
+ }
}
diff --git a/src/Symfony/Component/JsonPath/JsonPathUtils.php b/src/Symfony/Component/JsonPath/JsonPathUtils.php
index b5ac2ae6b8d0a..30bf446b6a9d5 100644
--- a/src/Symfony/Component/JsonPath/JsonPathUtils.php
+++ b/src/Symfony/Component/JsonPath/JsonPathUtils.php
@@ -85,4 +85,146 @@ public static function findSmallestDeserializableStringAndPath(array $tokens, mi
'tokens' => $remainingTokens,
];
}
+
+ public static function unescapeString(string $str, string $quoteChar): string
+ {
+ if ('"' === $quoteChar) {
+ // try JSON decoding first for unicode sequences
+ $jsonStr = '"'.$str.'"';
+ $decoded = json_decode($jsonStr, true);
+
+ if (null !== $decoded) {
+ return $decoded;
+ }
+ }
+
+ $result = '';
+ $i = -1;
+
+ while (null !== $char = $str[++$i] ?? null) {
+ if ('\\' === $char && isset($str[$i + 1])) {
+ $result .= match ($str[$i + 1]) {
+ '"' => '"',
+ "'" => "'",
+ '\\' => '\\',
+ '/' => '/',
+ 'b' => "\b",
+ 'f' => "\f",
+ 'n' => "\n",
+ 'r' => "\r",
+ 't' => "\t",
+ 'u' => self::unescapeUnicodeSequence($str, $i),
+ default => $char.$str[$i + 1], // keep the backslash
+ };
+
+ ++$i;
+ } else {
+ $result .= $char;
+ }
+ }
+
+ return $result;
+ }
+
+ private static function unescapeUnicodeSequence(string $str, int &$i): string
+ {
+ if (!isset($str[$i + 5])) {
+ // not enough characters for Unicode escape, treat as literal
+ return $str[$i];
+ }
+
+ $hex = substr($str, $i + 2, 4);
+ if (!ctype_xdigit($hex)) {
+ // invalid hex, treat as literal
+ return $str[$i];
+ }
+
+ $codepoint = hexdec($hex);
+ // looks like a valid Unicode codepoint, string length is sufficient and it starts with \u
+ if (0xD800 <= $codepoint && $codepoint <= 0xDBFF && isset($str[$i + 11]) && '\\' === $str[$i + 6] && 'u' === $str[$i + 7]) {
+ $lowHex = substr($str, $i + 8, 4);
+ if (ctype_xdigit($lowHex)) {
+ $lowSurrogate = hexdec($lowHex);
+ if (0xDC00 <= $lowSurrogate && $lowSurrogate <= 0xDFFF) {
+ $codepoint = 0x10000 + (($codepoint & 0x3FF) << 10) + ($lowSurrogate & 0x3FF);
+ $i += 10; // skip surrogate pair
+
+ return mb_chr($codepoint, 'UTF-8');
+ }
+ }
+ }
+
+ // single Unicode character or invalid surrogate, skip the sequence
+ $i += 4;
+
+ return mb_chr($codepoint, 'UTF-8');
+ }
+
+ /**
+ * @see https://datatracker.ietf.org/doc/rfc9535/, section 2.1.1
+ */
+ public static function normalizeWhitespace(string $input): string
+ {
+ $normalized = strtr($input, [
+ "\t" => ' ',
+ "\n" => ' ',
+ "\r" => ' ',
+ ]);
+
+ return trim($normalized);
+ }
+
+ /**
+ * Check a number is RFC 9535 compliant using strict JSON number format.
+ */
+ public static function isJsonNumber(string $value): bool
+ {
+ return preg_match('/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/', $value);
+ }
+
+ public static function parseCommaSeparatedValues(string $expr): array
+ {
+ $parts = [];
+ $current = '';
+ $inQuotes = false;
+ $quoteChar = null;
+ $bracketDepth = 0;
+ $i = -1;
+
+ while (null !== $char = $expr[++$i] ?? null) {
+ if ('\\' === $char && isset($expr[$i + 1])) {
+ $current .= $char.$expr[++$i];
+ continue;
+ }
+
+ if ('"' === $char || "'" === $char) {
+ if (!$inQuotes) {
+ $inQuotes = true;
+ $quoteChar = $char;
+ } elseif ($char === $quoteChar) {
+ $inQuotes = false;
+ $quoteChar = null;
+ }
+ } elseif (!$inQuotes) {
+ if ('[' === $char) {
+ ++$bracketDepth;
+ } elseif (']' === $char) {
+ --$bracketDepth;
+ } elseif (0 === $bracketDepth && ',' === $char) {
+ $parts[] = trim($current);
+ $current = '';
+
+ continue;
+ }
+ }
+
+ $current .= $char;
+ }
+
+ if ('' !== $current) {
+ $parts[] = trim($current);
+ }
+
+ return $parts;
+ }
}
diff --git a/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile b/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile
new file mode 100644
index 0000000000000..d9b4c353f4a76
--- /dev/null
+++ b/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile
@@ -0,0 +1,9 @@
+override hash := 05f6cac786bf0cce95437e6f1adedc3186d54a71
+
+.PHONY: cts.json
+cts.json:
+ curl -f https://raw.githubusercontent.com/jsonpath-standard/jsonpath-compliance-test-suite/$(hash)/cts.json -o cts.json
+
+.PHONY: clean
+clean:
+ rm -f cts.json
diff --git a/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json b/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json
new file mode 100644
index 0000000000000..363dce7893ca6
--- /dev/null
+++ b/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json
@@ -0,0 +1,12702 @@
+{
+ "description": "JSONPath Compliance Test Suite. This file is autogenerated, do not edit.",
+ "tests": [
+ {
+ "name": "basic, root",
+ "selector": "$",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ [
+ "first",
+ "second"
+ ]
+ ],
+ "result_paths": [
+ "$"
+ ]
+ },
+ {
+ "name": "basic, no leading whitespace",
+ "selector": " $",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "basic, no trailing whitespace",
+ "selector": "$ ",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "basic, name shorthand",
+ "selector": "$.a",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "basic, name shorthand, extended unicode ☺",
+ "selector": "$.☺",
+ "document": {
+ "☺": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ]
+ },
+ {
+ "name": "basic, name shorthand, underscore",
+ "selector": "$._",
+ "document": {
+ "_": "A",
+ "_foo": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['_']"
+ ]
+ },
+ {
+ "name": "basic, name shorthand, symbol",
+ "selector": "$.&",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, name shorthand, number",
+ "selector": "$.1",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, name shorthand, absent data",
+ "selector": "$.c",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "basic, name shorthand, array data",
+ "selector": "$.a",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "basic, name shorthand, object data, nested",
+ "selector": "$.a.b.c",
+ "document": {
+ "a": {
+ "b": {
+ "c": "C"
+ }
+ }
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['a']['b']['c']"
+ ]
+ },
+ {
+ "name": "basic, wildcard shorthand, object data",
+ "selector": "$.*",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "results": [
+ [
+ "A",
+ "B"
+ ],
+ [
+ "B",
+ "A"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, wildcard shorthand, array data",
+ "selector": "$.*",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first",
+ "second"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, wildcard selector, array data",
+ "selector": "$[*]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first",
+ "second"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, wildcard shorthand, then name shorthand",
+ "selector": "$.*.a",
+ "document": {
+ "x": {
+ "a": "Ax",
+ "b": "Bx"
+ },
+ "y": {
+ "a": "Ay",
+ "b": "By"
+ }
+ },
+ "results": [
+ [
+ "Ax",
+ "Ay"
+ ],
+ [
+ "Ay",
+ "Ax"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['x']['a']",
+ "$['y']['a']"
+ ],
+ [
+ "$['y']['a']",
+ "$['x']['a']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, multiple selectors",
+ "selector": "$[0,2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 2
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, space instead of comma",
+ "selector": "$[0 2]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "basic, selector, leading comma",
+ "selector": "$[,0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, selector, trailing comma",
+ "selector": "$[0,]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, multiple selectors, name and index, array data",
+ "selector": "$['a',1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, name and index, object data",
+ "selector": "$['a',1]",
+ "document": {
+ "a": 1,
+ "b": 2
+ },
+ "result": [
+ 1
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, index and slice",
+ "selector": "$[1,5:7]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 5,
+ 6
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[5]",
+ "$[6]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, index and slice, overlapping",
+ "selector": "$[1,0:3]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 0,
+ 1,
+ 2
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[0]",
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, duplicate index",
+ "selector": "$[1,1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 1
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, wildcard and index",
+ "selector": "$[*,1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, wildcard and name",
+ "selector": "$[*,'a']",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "results": [
+ [
+ "A",
+ "B",
+ "A"
+ ],
+ [
+ "B",
+ "A",
+ "A"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']",
+ "$['a']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, wildcard and slice",
+ "selector": "$[*,0:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]",
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, multiple selectors, multiple wildcards",
+ "selector": "$[*,*]",
+ "document": [
+ 0,
+ 1,
+ 2
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 0,
+ 1,
+ 2
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[0]",
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "basic, empty segment",
+ "selector": "$[]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, descendant segment, index",
+ "selector": "$..[1]",
+ "document": {
+ "o": [
+ 0,
+ 1,
+ [
+ 2,
+ 3
+ ]
+ ]
+ },
+ "result": [
+ 1,
+ 3
+ ],
+ "result_paths": [
+ "$['o'][1]",
+ "$['o'][2][1]"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, name shorthand",
+ "selector": "$..a",
+ "document": {
+ "o": [
+ {
+ "a": "b"
+ },
+ {
+ "a": "c"
+ }
+ ]
+ },
+ "result": [
+ "b",
+ "c"
+ ],
+ "result_paths": [
+ "$['o'][0]['a']",
+ "$['o'][1]['a']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard shorthand, array data",
+ "selector": "$..*",
+ "document": [
+ 0,
+ 1
+ ],
+ "result": [
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard selector, array data",
+ "selector": "$..[*]",
+ "document": [
+ 0,
+ 1
+ ],
+ "result": [
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard selector, nested arrays",
+ "selector": "$..[*]",
+ "document": [
+ [
+ [
+ 1
+ ]
+ ],
+ [
+ 2
+ ]
+ ],
+ "results": [
+ [
+ [
+ [
+ 1
+ ]
+ ],
+ [
+ 2
+ ],
+ [
+ 1
+ ],
+ 1,
+ 2
+ ],
+ [
+ [
+ [
+ 1
+ ]
+ ],
+ [
+ 2
+ ],
+ [
+ 1
+ ],
+ 2,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$[0]",
+ "$[1]",
+ "$[0][0]",
+ "$[0][0][0]",
+ "$[1][0]"
+ ],
+ [
+ "$[0]",
+ "$[1]",
+ "$[0][0]",
+ "$[1][0]",
+ "$[0][0][0]"
+ ]
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard selector, nested objects",
+ "selector": "$..[*]",
+ "document": {
+ "a": {
+ "c": {
+ "e": 1
+ }
+ },
+ "b": {
+ "d": 2
+ }
+ },
+ "results": [
+ [
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "d": 2
+ },
+ {
+ "e": 1
+ },
+ 1,
+ 2
+ ],
+ [
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "d": 2
+ },
+ {
+ "e": 1
+ },
+ 2,
+ 1
+ ],
+ [
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "d": 2
+ },
+ 2,
+ {
+ "e": 1
+ },
+ 1
+ ],
+ [
+ {
+ "d": 2
+ },
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "e": 1
+ },
+ 1,
+ 2
+ ],
+ [
+ {
+ "d": 2
+ },
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ {
+ "e": 1
+ },
+ 2,
+ 1
+ ],
+ [
+ {
+ "d": 2
+ },
+ {
+ "c": {
+ "e": 1
+ }
+ },
+ 2,
+ {
+ "e": 1
+ },
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']",
+ "$['a']['c']",
+ "$['a']['c']['e']",
+ "$['b']['d']"
+ ],
+ [
+ "$['a']",
+ "$['b']",
+ "$['a']['c']",
+ "$['b']['d']",
+ "$['a']['c']['e']"
+ ],
+ [
+ "$['a']",
+ "$['b']",
+ "$['b']['d']",
+ "$['a']['c']",
+ "$['a']['c']['e']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['a']['c']",
+ "$['a']['c']['e']",
+ "$['b']['d']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['a']['c']",
+ "$['b']['d']",
+ "$['a']['c']['e']"
+ ],
+ [
+ "$['b']",
+ "$['a']",
+ "$['b']['d']",
+ "$['a']['c']",
+ "$['a']['c']['e']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard shorthand, object data",
+ "selector": "$..*",
+ "document": {
+ "a": "b"
+ },
+ "result": [
+ "b"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, wildcard shorthand, nested data",
+ "selector": "$..*",
+ "document": {
+ "o": [
+ {
+ "a": "b"
+ }
+ ]
+ },
+ "result": [
+ [
+ {
+ "a": "b"
+ }
+ ],
+ {
+ "a": "b"
+ },
+ "b"
+ ],
+ "result_paths": [
+ "$['o']",
+ "$['o'][0]",
+ "$['o'][0]['a']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, multiple selectors",
+ "selector": "$..['a','d']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ "b",
+ "e",
+ "c",
+ "f"
+ ],
+ "result_paths": [
+ "$[0]['a']",
+ "$[0]['d']",
+ "$[1]['a']",
+ "$[1]['d']"
+ ]
+ },
+ {
+ "name": "basic, descendant segment, object traversal, multiple selectors",
+ "selector": "$..['a','d']",
+ "document": {
+ "x": {
+ "a": "b",
+ "d": "e"
+ },
+ "y": {
+ "a": "c",
+ "d": "f"
+ }
+ },
+ "results": [
+ [
+ "b",
+ "e",
+ "c",
+ "f"
+ ],
+ [
+ "c",
+ "f",
+ "b",
+ "e"
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['x']['a']",
+ "$['x']['d']",
+ "$['y']['a']",
+ "$['y']['d']"
+ ],
+ [
+ "$['y']['a']",
+ "$['y']['d']",
+ "$['x']['a']",
+ "$['x']['d']"
+ ]
+ ]
+ },
+ {
+ "name": "basic, bald descendant segment",
+ "selector": "$..",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, current node identifier without filter selector",
+ "selector": "$[@.a]",
+ "invalid_selector": true
+ },
+ {
+ "name": "basic, root node identifier in brackets without filter selector",
+ "selector": "$[$.a]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, existence, without segments",
+ "selector": "$[?@]",
+ "document": {
+ "a": 1,
+ "b": null
+ },
+ "results": [
+ [
+ 1,
+ null
+ ],
+ [
+ null,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "filter, existence",
+ "selector": "$[?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, existence, present with null",
+ "selector": "$[?@.a]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": null,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, absolute existence, without segments",
+ "selector": "$[?$]",
+ "document": {
+ "a": 1,
+ "b": null
+ },
+ "results": [
+ [
+ 1,
+ null
+ ],
+ [
+ null,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "filter, absolute existence, with segments",
+ "selector": "$[?$.*.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, equals string, single quotes",
+ "selector": "$[?@.a=='b']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals numeric string, single quotes",
+ "selector": "$[?@.a=='1']",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "1",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals string, double quotes",
+ "selector": "$[?@.a==\"b\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals numeric string, double quotes",
+ "selector": "$[?@.a==\"1\"]",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "1",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number",
+ "selector": "$[?@.a==1]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals null",
+ "selector": "$[?@.a==null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": null,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals null, absent from data",
+ "selector": "$[?@.a==null]",
+ "document": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, equals true",
+ "selector": "$[?@.a==true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": true,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals false",
+ "selector": "$[?@.a==false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals self",
+ "selector": "$[?@==@]",
+ "document": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]"
+ ]
+ },
+ {
+ "name": "filter, absolute, equals self",
+ "selector": "$[?$==$]",
+ "document": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result": [
+ 1,
+ null,
+ true,
+ {
+ "a": "b"
+ },
+ [
+ false
+ ]
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]"
+ ]
+ },
+ {
+ "name": "filter, equals, absent from index selector equals absent from name selector",
+ "selector": "$[?@.absent==@.list[9]]",
+ "document": [
+ {
+ "list": [
+ 1
+ ]
+ }
+ ],
+ "result": [
+ {
+ "list": [
+ 1
+ ]
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, deep equality, arrays",
+ "selector": "$[?@.a==@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": [
+ 1,
+ 2
+ ]
+ },
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ]
+ },
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ [
+ 2
+ ],
+ 1
+ ]
+ ]
+ },
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ 1,
+ 2
+ ]
+ ]
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ],
+ "b": [
+ [
+ 1,
+ [
+ 2
+ ]
+ ]
+ ]
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, deep equality, objects",
+ "selector": "$[?@.a==@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "y": {
+ "z": 1
+ },
+ "x": 1
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 2
+ }
+ }
+ }
+ ],
+ "result": [
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ }
+ },
+ {
+ "a": {
+ "x": 1,
+ "y": {
+ "z": 1
+ }
+ },
+ "b": {
+ "y": {
+ "z": 1
+ },
+ "x": 1
+ }
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, single quotes",
+ "selector": "$[?@.a!='b']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals numeric string, single quotes",
+ "selector": "$[?@.a!='1']",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, single quotes, different type",
+ "selector": "$[?@.a!='b']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, double quotes",
+ "selector": "$[?@.a!=\"b\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals numeric string, double quotes",
+ "selector": "$[?@.a!=\"1\"]",
+ "document": [
+ {
+ "a": "1",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals string, double quotes, different types",
+ "selector": "$[?@.a!=\"b\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals number",
+ "selector": "$[?@.a!=1]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, not-equals number, different types",
+ "selector": "$[?@.a!=1]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals null",
+ "selector": "$[?@.a!=null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals null, absent from data",
+ "selector": "$[?@.a!=null]",
+ "document": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals true",
+ "selector": "$[?@.a!=true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not-equals false",
+ "selector": "$[?@.a!=false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, less than string, single quotes",
+ "selector": "$[?@.a<'c']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, less than string, double quotes",
+ "selector": "$[?@.a<\"c\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, less than number",
+ "selector": "$[?@.a<10]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, less than null",
+ "selector": "$[?@.a'c']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than string, double quotes",
+ "selector": "$[?@.a>\"c\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than number",
+ "selector": "$[?@.a>10]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, greater than null",
+ "selector": "$[?@.a>null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, greater than true",
+ "selector": "$[?@.a>true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, greater than false",
+ "selector": "$[?@.a>false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, greater than or equal to string, single quotes",
+ "selector": "$[?@.a>='c']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to string, double quotes",
+ "selector": "$[?@.a>=\"c\"]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to number",
+ "selector": "$[?@.a>=10]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 10,
+ "d": "e"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to null",
+ "selector": "$[?@.a>=null]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": null,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to true",
+ "selector": "$[?@.a>=true]",
+ "document": [
+ {
+ "a": true,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": true,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, greater than or equal to false",
+ "selector": "$[?@.a>=false]",
+ "document": [
+ {
+ "a": false,
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, exists and not-equals null, absent from data",
+ "selector": "$[?@.a&&@.a!=null]",
+ "document": [
+ {
+ "d": "e"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, exists and exists, data false",
+ "selector": "$[?@.a&&@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": false
+ },
+ {
+ "b": false
+ },
+ {
+ "c": false
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "b": false
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, exists or exists, data false",
+ "selector": "$[?@.a||@.b]",
+ "document": [
+ {
+ "a": false,
+ "b": false
+ },
+ {
+ "b": false
+ },
+ {
+ "c": false
+ }
+ ],
+ "result": [
+ {
+ "a": false,
+ "b": false
+ },
+ {
+ "b": false
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, and",
+ "selector": "$[?@.a>0&&@.a<10]",
+ "document": [
+ {
+ "a": -10,
+ "d": "e"
+ },
+ {
+ "a": 5,
+ "d": "f"
+ },
+ {
+ "a": 20,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": 5,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, or",
+ "selector": "$[?@.a=='b'||@.a=='d']",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "c",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, not expression",
+ "selector": "$[?!(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, not exists",
+ "selector": "$[?!@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, not exists, data null",
+ "selector": "$[?!@.a]",
+ "document": [
+ {
+ "a": null,
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, wildcard",
+ "selector": "$[?@.*]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ {},
+ {
+ "a": 3
+ }
+ ],
+ "result": [
+ [
+ 2
+ ],
+ {
+ "a": 3
+ }
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[4]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, multiple",
+ "selector": "$[?@[0, 0, 'a']]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ [
+ 2,
+ 3
+ ],
+ {
+ "a": 3
+ },
+ {
+ "b": 4
+ },
+ {
+ "a": 3,
+ "b": 4
+ }
+ ],
+ "result": [
+ [
+ 2
+ ],
+ [
+ 2,
+ 3
+ ],
+ {
+ "a": 3
+ },
+ {
+ "a": 3,
+ "b": 4
+ }
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[6]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, slice",
+ "selector": "$[?@[0:2]]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ [
+ 2,
+ 3,
+ 4
+ ],
+ {},
+ {
+ "a": 3
+ }
+ ],
+ "result": [
+ [
+ 2
+ ],
+ [
+ 2,
+ 3,
+ 4
+ ]
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, non-singular existence, negated",
+ "selector": "$[?!@.*]",
+ "document": [
+ 1,
+ [],
+ [
+ 2
+ ],
+ {},
+ {
+ "a": 3
+ }
+ ],
+ "result": [
+ 1,
+ [],
+ {}
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, non-singular query in comparison, slice",
+ "selector": "$[?@[0:0]==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, non-singular query in comparison, all children",
+ "selector": "$[?@[*]==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, non-singular query in comparison, descendants",
+ "selector": "$[?@..a==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, non-singular query in comparison, combined",
+ "selector": "$[?@.a[*].a==0]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, nested",
+ "selector": "$[?@[?@>1]]",
+ "document": [
+ [
+ 0
+ ],
+ [
+ 0,
+ 1
+ ],
+ [
+ 0,
+ 1,
+ 2
+ ],
+ [
+ 42
+ ]
+ ],
+ "result": [
+ [
+ 0,
+ 1,
+ 2
+ ],
+ [
+ 42
+ ]
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]"
+ ]
+ },
+ {
+ "name": "filter, name segment on primitive, selects nothing",
+ "selector": "$[?@.a == 1]",
+ "document": {
+ "a": 1
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, name segment on array, selects nothing",
+ "selector": "$[?@['0'] == 5]",
+ "document": [
+ [
+ 5,
+ 6
+ ]
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, index segment on object, selects nothing",
+ "selector": "$[?@[0] == 5]",
+ "document": [
+ {
+ "0": 5
+ }
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "filter, followed by name selector",
+ "selector": "$[?@.a==1].b.x",
+ "document": [
+ {
+ "a": 1,
+ "b": {
+ "x": 2
+ }
+ }
+ ],
+ "result": [
+ 2
+ ],
+ "result_paths": [
+ "$[0]['b']['x']"
+ ]
+ },
+ {
+ "name": "filter, followed by child segment that selects multiple elements",
+ "selector": "$[?@.z=='_']['x','y']",
+ "document": [
+ {
+ "x": 1,
+ "y": null,
+ "z": "_"
+ }
+ ],
+ "result": [
+ 1,
+ null
+ ],
+ "result_paths": [
+ "$[0]['x']",
+ "$[0]['y']"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, index, equal",
+ "selector": "$[?(@[0, 0]==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, index, not equal",
+ "selector": "$[?(@[0, 0]!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, index, less-or-equal",
+ "selector": "$[?(@[0, 0]<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, name, equal",
+ "selector": "$[?(@['a', 'a']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, name, not equal",
+ "selector": "$[?(@['a', 'a']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, name, less-or-equal",
+ "selector": "$[?(@['a', 'a']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, combined, equal",
+ "selector": "$[?(@[0, '0']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, combined, not equal",
+ "selector": "$[?(@[0, '0']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, combined, less-or-equal",
+ "selector": "$[?(@[0, '0']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, relative non-singular query, wildcard, equal",
+ "selector": "$[?(@.*==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, wildcard, not equal",
+ "selector": "$[?(@.*!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, wildcard, less-or-equal",
+ "selector": "$[?(@.*<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, slice, equal",
+ "selector": "$[?(@[0:0]==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, slice, not equal",
+ "selector": "$[?(@[0:0]!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, relative non-singular query, slice, less-or-equal",
+ "selector": "$[?(@[0:0]<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, index, equal",
+ "selector": "$[?($[0, 0]==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, index, not equal",
+ "selector": "$[?($[0, 0]!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, index, less-or-equal",
+ "selector": "$[?($[0, 0]<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, name, equal",
+ "selector": "$[?($['a', 'a']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, name, not equal",
+ "selector": "$[?($['a', 'a']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, name, less-or-equal",
+ "selector": "$[?($['a', 'a']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, combined, equal",
+ "selector": "$[?($[0, '0']==42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, combined, not equal",
+ "selector": "$[?($[0, '0']!=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, combined, less-or-equal",
+ "selector": "$[?($[0, '0']<=42)]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, absolute non-singular query, wildcard, equal",
+ "selector": "$[?($.*==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, wildcard, not equal",
+ "selector": "$[?($.*!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, wildcard, less-or-equal",
+ "selector": "$[?($.*<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, slice, equal",
+ "selector": "$[?($[0:0]==42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, slice, not equal",
+ "selector": "$[?($[0:0]!=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, absolute non-singular query, slice, less-or-equal",
+ "selector": "$[?($[0:0]<=42)]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, multiple selectors",
+ "selector": "$[?@.a,?@.b]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, comparison",
+ "selector": "$[?@.a=='b',?@.b=='x']",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, overlapping",
+ "selector": "$[?@.a,?@.d]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, filter and index",
+ "selector": "$[?@.a,1]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, filter and wildcard",
+ "selector": "$[?@.a,*]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[0]",
+ "$[1]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, filter and slice",
+ "selector": "$[?@.a,1:]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ },
+ {
+ "g": "h"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ },
+ {
+ "g": "h"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, multiple selectors, comparison filter, index and slice",
+ "selector": "$[1, ?@.a=='b', 1:]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "b": "c",
+ "d": "f"
+ },
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, equals number, zero and negative zero",
+ "selector": "$[?@.a==0]",
+ "document": [
+ {
+ "a": 0,
+ "d": "e"
+ },
+ {
+ "a": 0.1,
+ "d": "f"
+ },
+ {
+ "a": "0",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, negative zero and zero",
+ "selector": "$[?@.a==-0]",
+ "document": [
+ {
+ "a": 0,
+ "d": "e"
+ },
+ {
+ "a": 0.1,
+ "d": "f"
+ },
+ {
+ "a": "0",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, with and without decimal fraction",
+ "selector": "$[?@.a==1.0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent",
+ "selector": "$[?@.a==1e2]",
+ "document": [
+ {
+ "a": 100,
+ "d": "e"
+ },
+ {
+ "a": 100.1,
+ "d": "f"
+ },
+ {
+ "a": "100",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 100,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent upper e",
+ "selector": "$[?@.a==1E2]",
+ "document": [
+ {
+ "a": 100,
+ "d": "e"
+ },
+ {
+ "a": 100.1,
+ "d": "f"
+ },
+ {
+ "a": "100",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 100,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, positive exponent",
+ "selector": "$[?@.a==1e+2]",
+ "document": [
+ {
+ "a": 100,
+ "d": "e"
+ },
+ {
+ "a": 100.1,
+ "d": "f"
+ },
+ {
+ "a": "100",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 100,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, negative exponent",
+ "selector": "$[?@.a==1e-2]",
+ "document": [
+ {
+ "a": 0.01,
+ "d": "e"
+ },
+ {
+ "a": 0.02,
+ "d": "f"
+ },
+ {
+ "a": "0.01",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0.01,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent 0",
+ "selector": "$[?@.a==1e0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent -0",
+ "selector": "$[?@.a==1e-0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent +0",
+ "selector": "$[?@.a==1e+0]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent leading -0",
+ "selector": "$[?@.a==1e-02]",
+ "document": [
+ {
+ "a": 0.01,
+ "d": "e"
+ },
+ {
+ "a": 0.02,
+ "d": "f"
+ },
+ {
+ "a": "0.01",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0.01,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, exponent +00",
+ "selector": "$[?@.a==1e+00]",
+ "document": [
+ {
+ "a": 1,
+ "d": "e"
+ },
+ {
+ "a": 2,
+ "d": "f"
+ },
+ {
+ "a": "1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction",
+ "selector": "$[?@.a==1.1]",
+ "document": [
+ {
+ "a": 1.1,
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ },
+ {
+ "a": "1.1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1.1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, trailing 0",
+ "selector": "$[?@.a==1.10]",
+ "document": [
+ {
+ "a": 1.1,
+ "d": "e"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ },
+ {
+ "a": "1.1",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 1.1,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, exponent",
+ "selector": "$[?@.a==1.1e2]",
+ "document": [
+ {
+ "a": 110,
+ "d": "e"
+ },
+ {
+ "a": 110.1,
+ "d": "f"
+ },
+ {
+ "a": "110",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 110,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, positive exponent",
+ "selector": "$[?@.a==1.1e+2]",
+ "document": [
+ {
+ "a": 110,
+ "d": "e"
+ },
+ {
+ "a": 110.1,
+ "d": "f"
+ },
+ {
+ "a": "110",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 110,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, decimal fraction, negative exponent",
+ "selector": "$[?@.a==1.1e-2]",
+ "document": [
+ {
+ "a": 0.011,
+ "d": "e"
+ },
+ {
+ "a": 0.012,
+ "d": "f"
+ },
+ {
+ "a": "0.011",
+ "d": "g"
+ }
+ ],
+ "result": [
+ {
+ "a": 0.011,
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, equals number, invalid plus",
+ "selector": "$[?@.a==+1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid minus space",
+ "selector": "$[?@.a==- 1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid double minus",
+ "selector": "$[?@.a==--1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no int digit",
+ "selector": "$[?@.a==.1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid minus no int digit",
+ "selector": "$[?@.a==-.1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid 00",
+ "selector": "$[?@.a==00]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid leading 0",
+ "selector": "$[?@.a==01]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no fractional digit",
+ "selector": "$[?@.a==1.]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid middle minus",
+ "selector": "$[?@.a==1.-1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no fractional digit e",
+ "selector": "$[?@.a==1.e1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no e digit",
+ "selector": "$[?@.a==1e]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid no e digit minus",
+ "selector": "$[?@.a==1e-]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid double e",
+ "selector": "$[?@.a==1eE1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid e digit double minus",
+ "selector": "$[?@.a==1e--1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid e digit plus minus",
+ "selector": "$[?@.a==1e+-1]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid e decimal",
+ "selector": "$[?@.a==1e2.3]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals number, invalid multi e",
+ "selector": "$[?@.a==1e2e3]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, equals, special nothing",
+ "selector": "$.values[?length(@.a) == value($..c)]",
+ "document": {
+ "c": "cd",
+ "values": [
+ {
+ "a": "ab"
+ },
+ {
+ "c": "d"
+ },
+ {
+ "a": null
+ }
+ ]
+ },
+ "result": [
+ {
+ "c": "d"
+ },
+ {
+ "a": null
+ }
+ ],
+ "result_paths": [
+ "$['values'][1]",
+ "$['values'][2]"
+ ],
+ "tags": [
+ "function"
+ ]
+ },
+ {
+ "name": "filter, equals, empty node list and empty node list",
+ "selector": "$[?@.a == @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ]
+ },
+ {
+ "name": "filter, equals, empty node list and special nothing",
+ "selector": "$[?@.a == length(@.b)]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, object data",
+ "selector": "$[?@<3]",
+ "document": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ },
+ "results": [
+ [
+ 1,
+ 2
+ ],
+ [
+ 2,
+ 1
+ ]
+ ],
+ "results_paths": [
+ [
+ "$['a']",
+ "$['b']"
+ ],
+ [
+ "$['b']",
+ "$['a']"
+ ]
+ ]
+ },
+ {
+ "name": "filter, and binds more tightly than or",
+ "selector": "$[?@.a || @.b && @.c]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "c": 3
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[4]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, left to right evaluation",
+ "selector": "$[?@.a && @.b || @.c]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 1,
+ "c": 3
+ },
+ {
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 1,
+ "c": 3
+ },
+ {
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, group terms, left",
+ "selector": "$[?(@.a || @.b) && @.c]",
+ "document": [
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "c": 3
+ },
+ {
+ "b": 2,
+ "c": 3
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]",
+ "$[6]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, group terms, right",
+ "selector": "$[?@.a && (@.b || @.c)]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 2
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 2
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 1,
+ "c": 2
+ },
+ {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]",
+ "$[5]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, string literal, single quote in double quotes",
+ "selector": "$[?@ == \"quoted' literal\"]",
+ "document": [
+ "quoted' literal",
+ "a",
+ "quoted\\' literal"
+ ],
+ "result": [
+ "quoted' literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ]
+ },
+ {
+ "name": "filter, string literal, double quote in single quotes",
+ "selector": "$[?@ == 'quoted\" literal']",
+ "document": [
+ "quoted\" literal",
+ "a",
+ "quoted\\\" literal",
+ "'quoted\" literal'"
+ ],
+ "result": [
+ "quoted\" literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, string literal, escaped single quote in single quotes",
+ "selector": "$[?@ == 'quoted\\' literal']",
+ "document": [
+ "quoted' literal",
+ "a",
+ "quoted\\' literal",
+ "'quoted\" literal'"
+ ],
+ "result": [
+ "quoted' literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, string literal, escaped double quote in double quotes",
+ "selector": "$[?@ == \"quoted\\\" literal\"]",
+ "document": [
+ "quoted\" literal",
+ "a",
+ "quoted\\\" literal",
+ "'quoted\" literal'"
+ ],
+ "result": [
+ "quoted\" literal"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, literal true must be compared",
+ "selector": "$[?true]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal false must be compared",
+ "selector": "$[?false]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal string must be compared",
+ "selector": "$[?'abc']",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal int must be compared",
+ "selector": "$[?2]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal float must be compared",
+ "selector": "$[?2.2]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, literal null must be compared",
+ "selector": "$[?null]",
+ "invalid_selector": true
+ },
+ {
+ "name": "filter, and, literals must be compared",
+ "selector": "$[?true && false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, or, literals must be compared",
+ "selector": "$[?true || false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, and, right hand literal must be compared",
+ "selector": "$[?true == false && false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, or, right hand literal must be compared",
+ "selector": "$[?true == false || false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, and, left hand literal must be compared",
+ "selector": "$[?false && true == false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, or, left hand literal must be compared",
+ "selector": "$[?false || true == false]",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "filter, true, incorrectly capitalized",
+ "selector": "$[?@==True]",
+ "invalid_selector": true,
+ "tags": [
+ "case"
+ ]
+ },
+ {
+ "name": "filter, false, incorrectly capitalized",
+ "selector": "$[?@==False]",
+ "invalid_selector": true,
+ "tags": [
+ "case"
+ ]
+ },
+ {
+ "name": "filter, null, incorrectly capitalized",
+ "selector": "$[?@==Null]",
+ "invalid_selector": true,
+ "tags": [
+ "case"
+ ]
+ },
+ {
+ "name": "index selector, first element",
+ "selector": "$[0]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, second element",
+ "selector": "$[1]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "second"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, out of bound",
+ "selector": "$[2]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, min exact index",
+ "selector": "$[-9007199254740991]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, max exact index",
+ "selector": "$[9007199254740991]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, min exact index - 1",
+ "selector": "$[-9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, max exact index + 1",
+ "selector": "$[9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, overflowing index",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, not actually an index, overflowing index leads into general text",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168SomeRandomText]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, negative",
+ "selector": "$[-1]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "second"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, more negative",
+ "selector": "$[-2]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [
+ "first"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, negative out of bound",
+ "selector": "$[-3]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, on object",
+ "selector": "$[0]",
+ "document": {
+ "foo": 1
+ },
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, leading 0",
+ "selector": "$[01]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, decimal",
+ "selector": "$[1.0]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, plus",
+ "selector": "$[+1]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, minus space",
+ "selector": "$[- 1]",
+ "invalid_selector": true,
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "index selector, -0",
+ "selector": "$[-0]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "index selector, leading -0",
+ "selector": "$[-01]",
+ "invalid_selector": true,
+ "tags": [
+ "index"
+ ]
+ },
+ {
+ "name": "name selector, double quotes",
+ "selector": "$[\"a\"]",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, absent data",
+ "selector": "$[\"c\"]",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, double quotes, array data",
+ "selector": "$[\"a\"]",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, name, double quotes, contains single quote",
+ "selector": "$[\"a'\"]",
+ "document": {
+ "a'": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a\\'']"
+ ]
+ },
+ {
+ "name": "name selector, name, double quotes, nested",
+ "selector": "$[\"a\"][\"b\"][\"c\"]",
+ "document": {
+ "a": {
+ "b": {
+ "c": "C"
+ }
+ }
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['a']['b']['c']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0000",
+ "selector": "$[\"\u0000\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0001",
+ "selector": "$[\"\u0001\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0002",
+ "selector": "$[\"\u0002\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0003",
+ "selector": "$[\"\u0003\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0004",
+ "selector": "$[\"\u0004\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0005",
+ "selector": "$[\"\u0005\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0006",
+ "selector": "$[\"\u0006\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0007",
+ "selector": "$[\"\u0007\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0008",
+ "selector": "$[\"\b\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0009",
+ "selector": "$[\"\t\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000A",
+ "selector": "$[\"\n\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000B",
+ "selector": "$[\"\u000b\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000C",
+ "selector": "$[\"\f\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000D",
+ "selector": "$[\"\r\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000E",
+ "selector": "$[\"\u000e\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+000F",
+ "selector": "$[\"\u000f\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0010",
+ "selector": "$[\"\u0010\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0011",
+ "selector": "$[\"\u0011\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0012",
+ "selector": "$[\"\u0012\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0013",
+ "selector": "$[\"\u0013\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0014",
+ "selector": "$[\"\u0014\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0015",
+ "selector": "$[\"\u0015\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0016",
+ "selector": "$[\"\u0016\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0017",
+ "selector": "$[\"\u0017\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0018",
+ "selector": "$[\"\u0018\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0019",
+ "selector": "$[\"\u0019\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001A",
+ "selector": "$[\"\u001a\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001B",
+ "selector": "$[\"\u001b\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001C",
+ "selector": "$[\"\u001c\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001D",
+ "selector": "$[\"\u001d\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001E",
+ "selector": "$[\"\u001e\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+001F",
+ "selector": "$[\"\u001f\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+0020",
+ "selector": "$[\" \"]",
+ "document": {
+ " ": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$[' ']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, embedded U+007F",
+ "selector": "$[\"\"]",
+ "document": {
+ "": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, supplementary plane character",
+ "selector": "$[\"𝄞\"]",
+ "document": {
+ "𝄞": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['𝄞']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped double quote",
+ "selector": "$[\"\\\"\"]",
+ "document": {
+ "\"": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\"']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped reverse solidus",
+ "selector": "$[\"\\\\\"]",
+ "document": {
+ "\\": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\\\']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped solidus",
+ "selector": "$[\"\\/\"]",
+ "document": {
+ "/": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['/']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped backspace",
+ "selector": "$[\"\\b\"]",
+ "document": {
+ "\b": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\b']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped form feed",
+ "selector": "$[\"\\f\"]",
+ "document": {
+ "\f": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\f']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped line feed",
+ "selector": "$[\"\\n\"]",
+ "document": {
+ "\n": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\n']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped carriage return",
+ "selector": "$[\"\\r\"]",
+ "document": {
+ "\r": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\r']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped tab",
+ "selector": "$[\"\\t\"]",
+ "document": {
+ "\t": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\t']"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped ☺, upper case hex",
+ "selector": "$[\"\\u263A\"]",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, escaped ☺, lower case hex",
+ "selector": "$[\"\\u263a\"]",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate pair 𝄞",
+ "selector": "$[\"\\uD834\\uDD1E\"]",
+ "document": {
+ "𝄞": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['𝄞']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate pair 😀",
+ "selector": "$[\"\\uD83D\\uDE00\"]",
+ "document": {
+ "😀": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['😀']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, before high surrogates",
+ "selector": "$[\"\\uD7FF\\uD7FF\"]",
+ "document": {
+ "": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, after low surrogates",
+ "selector": "$[\"\\uE000\\uE000\"]",
+ "document": {
+ "": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, invalid escaped single quote",
+ "selector": "$[\"\\'\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, embedded double quote",
+ "selector": "$[\"\"\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, incomplete escape",
+ "selector": "$[\"\\\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, escape at end of line",
+ "selector": "$[\"\\\n\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, question mark escape",
+ "selector": "$[\"\\?\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, bell escape",
+ "selector": "$[\"\\a\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, vertical tab escape",
+ "selector": "$[\"\\v\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, 0 escape",
+ "selector": "$[\"\\0\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, x escape",
+ "selector": "$[\"\\x12\"]",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, n escape",
+ "selector": "$[\"\\N{LATIN CAPITAL LETTER A}\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape no hex",
+ "selector": "$[\"\\u\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape too few hex",
+ "selector": "$[\"\\u123\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape upper u",
+ "selector": "$[\"\\U1234\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape upper u long",
+ "selector": "$[\"\\U0010FFFF\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape plus",
+ "selector": "$[\"\\u+1234\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape brackets",
+ "selector": "$[\"\\u{1234}\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, unicode escape brackets long",
+ "selector": "$[\"\\u{10ffff}\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, single high surrogate",
+ "selector": "$[\"\\uD800\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, single low surrogate",
+ "selector": "$[\"\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, high high surrogate",
+ "selector": "$[\"\\uD800\\uD800\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, low low surrogate",
+ "selector": "$[\"\\uDC00\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate non-surrogate",
+ "selector": "$[\"\\uD800\\u1234\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, non-surrogate surrogate",
+ "selector": "$[\"\\u1234\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate supplementary",
+ "selector": "$[\"\\uD800𝄞\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, supplementary surrogate",
+ "selector": "$[\"𝄞\\uDC00\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, double quotes, surrogate incomplete low",
+ "selector": "$[\"\\uD800\\uDC0\"]",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes",
+ "selector": "$['a']",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['a']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, absent data",
+ "selector": "$['c']",
+ "document": {
+ "a": "A",
+ "b": "B"
+ },
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, single quotes, array data",
+ "selector": "$['a']",
+ "document": [
+ "first",
+ "second"
+ ],
+ "result": [],
+ "result_paths": []
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0000",
+ "selector": "$['\u0000']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0001",
+ "selector": "$['\u0001']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0002",
+ "selector": "$['\u0002']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0003",
+ "selector": "$['\u0003']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0004",
+ "selector": "$['\u0004']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0005",
+ "selector": "$['\u0005']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0006",
+ "selector": "$['\u0006']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0007",
+ "selector": "$['\u0007']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0008",
+ "selector": "$['\b']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0009",
+ "selector": "$['\t']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000A",
+ "selector": "$['\n']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000B",
+ "selector": "$['\u000b']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000C",
+ "selector": "$['\f']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000D",
+ "selector": "$['\r']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000E",
+ "selector": "$['\u000e']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+000F",
+ "selector": "$['\u000f']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0010",
+ "selector": "$['\u0010']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0011",
+ "selector": "$['\u0011']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0012",
+ "selector": "$['\u0012']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0013",
+ "selector": "$['\u0013']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0014",
+ "selector": "$['\u0014']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0015",
+ "selector": "$['\u0015']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0016",
+ "selector": "$['\u0016']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0017",
+ "selector": "$['\u0017']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0018",
+ "selector": "$['\u0018']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0019",
+ "selector": "$['\u0019']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001A",
+ "selector": "$['\u001a']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001B",
+ "selector": "$['\u001b']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001C",
+ "selector": "$['\u001c']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001D",
+ "selector": "$['\u001d']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001E",
+ "selector": "$['\u001e']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+001F",
+ "selector": "$['\u001f']",
+ "invalid_selector": true,
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, embedded U+0020",
+ "selector": "$[' ']",
+ "document": {
+ " ": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$[' ']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped single quote",
+ "selector": "$['\\'']",
+ "document": {
+ "'": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\'']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped reverse solidus",
+ "selector": "$['\\\\']",
+ "document": {
+ "\\": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\\\']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped solidus",
+ "selector": "$['\\/']",
+ "document": {
+ "/": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['/']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped backspace",
+ "selector": "$['\\b']",
+ "document": {
+ "\b": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\b']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped form feed",
+ "selector": "$['\\f']",
+ "document": {
+ "\f": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\f']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped line feed",
+ "selector": "$['\\n']",
+ "document": {
+ "\n": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\n']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped carriage return",
+ "selector": "$['\\r']",
+ "document": {
+ "\r": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\r']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped tab",
+ "selector": "$['\\t']",
+ "document": {
+ "\t": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['\\t']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped ☺, upper case hex",
+ "selector": "$['\\u263A']",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, escaped ☺, lower case hex",
+ "selector": "$['\\u263a']",
+ "document": {
+ "☺": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['☺']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, surrogate pair 𝄞",
+ "selector": "$['\\uD834\\uDD1E']",
+ "document": {
+ "𝄞": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['𝄞']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, surrogate pair 😀",
+ "selector": "$['\\uD83D\\uDE00']",
+ "document": {
+ "😀": "A"
+ },
+ "result": [
+ "A"
+ ],
+ "result_paths": [
+ "$['😀']"
+ ],
+ "tags": [
+ "unicode"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, invalid escaped double quote",
+ "selector": "$['\\\"']",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, single quotes, embedded single quote",
+ "selector": "$[''']",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, single quotes, incomplete escape",
+ "selector": "$['\\']",
+ "invalid_selector": true
+ },
+ {
+ "name": "name selector, double quotes, empty",
+ "selector": "$[\"\"]",
+ "document": {
+ "a": "A",
+ "b": "B",
+ "": "C"
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['']"
+ ]
+ },
+ {
+ "name": "name selector, single quotes, empty",
+ "selector": "$['']",
+ "document": {
+ "a": "A",
+ "b": "B",
+ "": "C"
+ },
+ "result": [
+ "C"
+ ],
+ "result_paths": [
+ "$['']"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector",
+ "selector": "$[1:3]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 2
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with step",
+ "selector": "$[1:6:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 3,
+ 5
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]",
+ "$[5]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with everything omitted, short form",
+ "selector": "$[:]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with everything omitted, long form",
+ "selector": "$[::]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with start omitted",
+ "selector": "$[:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with start and end omitted",
+ "selector": "$[::2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 8
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]",
+ "$[4]",
+ "$[6]",
+ "$[8]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with default start and end",
+ "selector": "$[::-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 3,
+ 2,
+ 1,
+ 0
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[2]",
+ "$[1]",
+ "$[0]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with default start",
+ "selector": "$[:0:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 3,
+ 2,
+ 1
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[2]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with default end",
+ "selector": "$[2::-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 2,
+ 1,
+ 0
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[1]",
+ "$[0]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, larger negative step",
+ "selector": "$[::-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "result": [
+ 3,
+ 1
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative range with default step",
+ "selector": "$[-1:-3]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative range with negative step",
+ "selector": "$[-1:-3:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative range with larger negative step",
+ "selector": "$[-1:-6:-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 7,
+ 5
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[7]",
+ "$[5]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, larger negative range with larger negative step",
+ "selector": "$[-1:-7:-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 7,
+ 5
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[7]",
+ "$[5]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from, positive to",
+ "selector": "$[-5:7]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 5,
+ 6
+ ],
+ "result_paths": [
+ "$[5]",
+ "$[6]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from",
+ "selector": "$[-2:]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 8,
+ 9
+ ],
+ "result_paths": [
+ "$[8]",
+ "$[9]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, positive from, negative to",
+ "selector": "$[1:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from, positive to, negative step",
+ "selector": "$[-1:1:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]",
+ "$[7]",
+ "$[6]",
+ "$[5]",
+ "$[4]",
+ "$[3]",
+ "$[2]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, positive from, negative to, negative step",
+ "selector": "$[7:-5:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 7,
+ 6
+ ],
+ "result_paths": [
+ "$[7]",
+ "$[6]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, in serial, on nested array",
+ "selector": "$[1:3][1:2]",
+ "document": [
+ [
+ "a",
+ "b",
+ "c"
+ ],
+ [
+ "d",
+ "e",
+ "f"
+ ],
+ [
+ "g",
+ "h",
+ "i"
+ ]
+ ],
+ "result": [
+ "e",
+ "h"
+ ],
+ "result_paths": [
+ "$[1][1]",
+ "$[2][1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, in serial, on flat array",
+ "selector": "$[1:3][::]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative from, negative to, positive step",
+ "selector": "$[-5:-2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 5,
+ 6,
+ 7
+ ],
+ "result_paths": [
+ "$[5]",
+ "$[6]",
+ "$[7]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, too many colons",
+ "selector": "$[1:2:3:4]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, non-integer array index",
+ "selector": "$[1:2:a]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, zero step",
+ "selector": "$[1:2:0]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, empty range",
+ "selector": "$[2:2]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, slice selector with everything omitted with empty array",
+ "selector": "$[:]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, negative step with empty array",
+ "selector": "$[::-1]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, maximal range with positive step",
+ "selector": "$[0:10]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]",
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, maximal range with negative step",
+ "selector": "$[9:0:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]",
+ "$[7]",
+ "$[6]",
+ "$[5]",
+ "$[4]",
+ "$[3]",
+ "$[2]",
+ "$[1]"
+ ],
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively large to value",
+ "selector": "$[2:113667776004]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result_paths": [
+ "$[2]",
+ "$[3]",
+ "$[4]",
+ "$[5]",
+ "$[6]",
+ "$[7]",
+ "$[8]",
+ "$[9]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively small from value",
+ "selector": "$[-113667776004:1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 0
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively large from value with negative step",
+ "selector": "$[113667776004:0:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ ],
+ "result_paths": [
+ "$[9]",
+ "$[8]",
+ "$[7]",
+ "$[6]",
+ "$[5]",
+ "$[4]",
+ "$[3]",
+ "$[2]",
+ "$[1]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively small to value with negative step",
+ "selector": "$[3:-113667776004:-1]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 3,
+ 2,
+ 1,
+ 0
+ ],
+ "result_paths": [
+ "$[3]",
+ "$[2]",
+ "$[1]",
+ "$[0]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively large step",
+ "selector": "$[1:10:113667776004]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 1
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, excessively small step",
+ "selector": "$[-1:-10:-113667776004]",
+ "document": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "result": [
+ 9
+ ],
+ "result_paths": [
+ "$[9]"
+ ],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, min exact",
+ "selector": "$[-9007199254740991::]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, max exact",
+ "selector": "$[9007199254740991::]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, min exact - 1",
+ "selector": "$[-9007199254740992::]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, max exact + 1",
+ "selector": "$[9007199254740992::]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, min exact",
+ "selector": "$[:-9007199254740991:]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, max exact",
+ "selector": "$[:9007199254740991:]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, min exact - 1",
+ "selector": "$[:-9007199254740992:]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, max exact + 1",
+ "selector": "$[:9007199254740992:]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, min exact",
+ "selector": "$[::-9007199254740991]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, max exact",
+ "selector": "$[::9007199254740991]",
+ "document": [],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, min exact - 1",
+ "selector": "$[::-9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, max exact + 1",
+ "selector": "$[::9007199254740992]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, overflowing to value",
+ "selector": "$[2:231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, underflowing from value",
+ "selector": "$[-231584178474632390847141970017375815706539969331281128078915168015826259279872:1]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, overflowing from value with negative step",
+ "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872:0:-1]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, underflowing to value with negative step",
+ "selector": "$[3:-231584178474632390847141970017375815706539969331281128078915168015826259279872:-1]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, overflowing step",
+ "selector": "$[1:10:231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, underflowing step",
+ "selector": "$[-1:-10:-231584178474632390847141970017375815706539969331281128078915168015826259279872]",
+ "invalid_selector": true,
+ "tags": [
+ "boundary",
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, leading 0",
+ "selector": "$[01::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, decimal",
+ "selector": "$[1.0::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, plus",
+ "selector": "$[+1::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, minus space",
+ "selector": "$[- 1::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, -0",
+ "selector": "$[-0::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, start, leading -0",
+ "selector": "$[-01::]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, leading 0",
+ "selector": "$[:01:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, decimal",
+ "selector": "$[:1.0:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, plus",
+ "selector": "$[:+1:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, minus space",
+ "selector": "$[:- 1:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, -0",
+ "selector": "$[:-0:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, end, leading -0",
+ "selector": "$[:-01:]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, leading 0",
+ "selector": "$[::01]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, decimal",
+ "selector": "$[::1.0]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, plus",
+ "selector": "$[::+1]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, minus space",
+ "selector": "$[::- 1]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, -0",
+ "selector": "$[::-0]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "slice selector, step, leading -0",
+ "selector": "$[::-01]",
+ "invalid_selector": true,
+ "tags": [
+ "slice"
+ ]
+ },
+ {
+ "name": "functions, count, count function",
+ "selector": "$[?count(@..*)>2]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, single-node arg",
+ "selector": "$[?count(@.a)>1]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, multiple-selector arg",
+ "selector": "$[?count(@['a','d'])>1]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ 1
+ ],
+ "d": "f"
+ },
+ {
+ "a": 1,
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[2]"
+ ],
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, number",
+ "selector": "$[?count(1)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, string",
+ "selector": "$[?count('string')>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, true",
+ "selector": "$[?count(true)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, false",
+ "selector": "$[?count(false)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, non-query arg, null",
+ "selector": "$[?count(null)>2]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, result must be compared",
+ "selector": "$[?count(@..*)]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, no params",
+ "selector": "$[?count()==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, count, too many params",
+ "selector": "$[?count(@.a,@.b)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function"
+ ]
+ },
+ {
+ "name": "functions, length, string data",
+ "selector": "$[?length(@.a)>=2]",
+ "document": [
+ {
+ "a": "ab"
+ },
+ {
+ "a": "d"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, string data, unicode",
+ "selector": "$[?length(@)==2]",
+ "document": [
+ "☺",
+ "☺☺",
+ "☺☺☺",
+ "ж",
+ "жж",
+ "жжж",
+ "磨",
+ "阿美",
+ "形声字"
+ ],
+ "result": [
+ "☺☺",
+ "жж",
+ "阿美"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[4]",
+ "$[7]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, array data",
+ "selector": "$[?length(@.a)>=2]",
+ "document": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ {
+ "a": [
+ 1
+ ]
+ }
+ ],
+ "result": [
+ {
+ "a": [
+ 1,
+ 2,
+ 3
+ ]
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, missing data",
+ "selector": "$[?length(@.a)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, number arg",
+ "selector": "$[?length(1)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, true arg",
+ "selector": "$[?length(true)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, false arg",
+ "selector": "$[?length(false)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, null arg",
+ "selector": "$[?length(null)>=2]",
+ "document": [
+ {
+ "d": "f"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, result must be compared",
+ "selector": "$[?length(@.a)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, no params",
+ "selector": "$[?length()==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, too many params",
+ "selector": "$[?length(@.a,@.b)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, non-singular query arg",
+ "selector": "$[?length(@.*)<3]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, arg is a function expression",
+ "selector": "$.values[?length(@.a)==length(value($..c))]",
+ "document": {
+ "c": "cd",
+ "values": [
+ {
+ "a": "ab"
+ },
+ {
+ "a": "d"
+ }
+ ]
+ },
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$['values'][0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, length, arg is special nothing",
+ "selector": "$[?length(value(@.a))>0]",
+ "document": [
+ {
+ "a": "ab"
+ },
+ {
+ "c": "d"
+ },
+ {
+ "a": null
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length"
+ ]
+ },
+ {
+ "name": "functions, match, found match",
+ "selector": "$[?match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, double quotes",
+ "selector": "$[?match(@.a, \"a.*\")]",
+ "document": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, regex from the document",
+ "selector": "$.values[?match(@, $.regex)]",
+ "document": {
+ "regex": "b.?b",
+ "values": [
+ "abc",
+ "bcd",
+ "bab",
+ "bba",
+ "bbab",
+ "b",
+ true,
+ [],
+ {}
+ ]
+ },
+ "result": [
+ "bab"
+ ],
+ "result_paths": [
+ "$['values'][2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, don't select match",
+ "selector": "$[?!match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, not a match",
+ "selector": "$[?match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, select non-match",
+ "selector": "$[?!match(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, non-string first arg",
+ "selector": "$[?match(1, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, non-string second arg",
+ "selector": "$[?match(@.a, 1)]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, filter, match function, unicode char class, uppercase",
+ "selector": "$[?match(@, '\\\\p{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ "жЖ",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "Ж"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, filter, match function, unicode char class negated, uppercase",
+ "selector": "$[?match(@, '\\\\P{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "ж",
+ "1"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, filter, match function, unicode, surrogate pair",
+ "selector": "$[?match(@, 'a.b')]",
+ "document": [
+ "a𐄁b",
+ "ab",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "a𐄁b"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, dot matcher on \\u2028",
+ "selector": "$[?match(@, '.')]",
+ "document": [
+ "
",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, dot matcher on \\u2029",
+ "selector": "$[?match(@, '.')]",
+ "document": [
+ "
",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, result cannot be compared",
+ "selector": "$[?match(@.a, 'a.*')==true]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, too few params",
+ "selector": "$[?match(@.a)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, too many params",
+ "selector": "$[?match(@.a,@.b,@.c)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, arg is a function expression",
+ "selector": "$.values[?match(@.a, value($..['regex']))]",
+ "document": {
+ "regex": "a.*",
+ "values": [
+ {
+ "a": "ab"
+ },
+ {
+ "a": "ba"
+ }
+ ]
+ },
+ "result": [
+ {
+ "a": "ab"
+ }
+ ],
+ "result_paths": [
+ "$['values'][0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, dot in character class",
+ "selector": "$[?match(@, 'a[.b]c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "axc"
+ ],
+ "result": [
+ "abc",
+ "a.c"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped dot",
+ "selector": "$[?match(@, 'a\\\\.c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "axc"
+ ],
+ "result": [
+ "a.c"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped backslash before dot",
+ "selector": "$[?match(@, 'a\\\\\\\\.c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "axc",
+ "a\\
c"
+ ],
+ "result": [
+ "a\\
c"
+ ],
+ "result_paths": [
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped left square bracket",
+ "selector": "$[?match(@, 'a\\\\[.c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "a[
c"
+ ],
+ "result": [
+ "a[
c"
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, escaped right square bracket",
+ "selector": "$[?match(@, 'a[\\\\].]c')]",
+ "document": [
+ "abc",
+ "a.c",
+ "a
c",
+ "a]c"
+ ],
+ "result": [
+ "a.c",
+ "a]c"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, explicit caret",
+ "selector": "$[?match(@, '^ab.*')]",
+ "document": [
+ "abc",
+ "axc",
+ "ab",
+ "xab"
+ ],
+ "result": [
+ "abc",
+ "ab"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, match, explicit dollar",
+ "selector": "$[?match(@, '.*bc$')]",
+ "document": [
+ "abc",
+ "axc",
+ "ab",
+ "abcx"
+ ],
+ "result": [
+ "abc"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "match"
+ ]
+ },
+ {
+ "name": "functions, search, at the end",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, double quotes",
+ "selector": "$[?search(@.a, \"a.*\")]",
+ "document": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result": [
+ {
+ "a": "the end is ab"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, at the start",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "ab is at the start"
+ }
+ ],
+ "result": [
+ {
+ "a": "ab is at the start"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, in the middle",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "contains two matches"
+ }
+ ],
+ "result": [
+ {
+ "a": "contains two matches"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, regex from the document",
+ "selector": "$.values[?search(@, $.regex)]",
+ "document": {
+ "regex": "b.?b",
+ "values": [
+ "abc",
+ "bcd",
+ "bab",
+ "bba",
+ "bbab",
+ "b",
+ true,
+ [],
+ {}
+ ]
+ },
+ "result": [
+ "bab",
+ "bba",
+ "bbab"
+ ],
+ "result_paths": [
+ "$['values'][2]",
+ "$['values'][3]",
+ "$['values'][4]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, don't select match",
+ "selector": "$[?!search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "contains two matches"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, not a match",
+ "selector": "$[?search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, select non-match",
+ "selector": "$[?!search(@.a, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, non-string first arg",
+ "selector": "$[?search(1, 'a.*')]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, non-string second arg",
+ "selector": "$[?search(@.a, 1)]",
+ "document": [
+ {
+ "a": "bc"
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, filter, search function, unicode char class, uppercase",
+ "selector": "$[?search(@, '\\\\p{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ "жЖ",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "Ж",
+ "жЖ"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, filter, search function, unicode char class negated, uppercase",
+ "selector": "$[?search(@, '\\\\P{Lu}')]",
+ "document": [
+ "ж",
+ "Ж",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "ж",
+ "1"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, filter, search function, unicode, surrogate pair",
+ "selector": "$[?search(@, 'a.b')]",
+ "document": [
+ "a𐄁bc",
+ "abc",
+ "1",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "a𐄁bc"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, dot matcher on \\u2028",
+ "selector": "$[?search(@, '.')]",
+ "document": [
+ "
",
+ "\r
\n",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
",
+ "\r
\n"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, dot matcher on \\u2029",
+ "selector": "$[?search(@, '.')]",
+ "document": [
+ "
",
+ "\r
\n",
+ "\r",
+ "\n",
+ true,
+ [],
+ {}
+ ],
+ "result": [
+ "
",
+ "\r
\n"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, result cannot be compared",
+ "selector": "$[?search(@.a, 'a.*')==true]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, too few params",
+ "selector": "$[?search(@.a)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, too many params",
+ "selector": "$[?search(@.a,@.b,@.c)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, arg is a function expression",
+ "selector": "$.values[?search(@, value($..['regex']))]",
+ "document": {
+ "regex": "b.?b",
+ "values": [
+ "abc",
+ "bcd",
+ "bab",
+ "bba",
+ "bbab",
+ "b",
+ true,
+ [],
+ {}
+ ]
+ },
+ "result": [
+ "bab",
+ "bba",
+ "bbab"
+ ],
+ "result_paths": [
+ "$['values'][2]",
+ "$['values'][3]",
+ "$['values'][4]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, dot in character class",
+ "selector": "$[?search(@, 'a[.b]c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x axc y"
+ ],
+ "result": [
+ "x abc y",
+ "x a.c y"
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped dot",
+ "selector": "$[?search(@, 'a\\\\.c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x axc y"
+ ],
+ "result": [
+ "x a.c y"
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped backslash before dot",
+ "selector": "$[?search(@, 'a\\\\\\\\.c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x axc y",
+ "x a\\
c y"
+ ],
+ "result": [
+ "x a\\
c y"
+ ],
+ "result_paths": [
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped left square bracket",
+ "selector": "$[?search(@, 'a\\\\[.c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x a[
c y"
+ ],
+ "result": [
+ "x a[
c y"
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, search, escaped right square bracket",
+ "selector": "$[?search(@, 'a[\\\\].]c')]",
+ "document": [
+ "x abc y",
+ "x a.c y",
+ "x a
c y",
+ "x a]c y"
+ ],
+ "result": [
+ "x a.c y",
+ "x a]c y"
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "function",
+ "search"
+ ]
+ },
+ {
+ "name": "functions, value, single-value nodelist",
+ "selector": "$[?value(@.*)==4]",
+ "document": [
+ [
+ 4
+ ],
+ {
+ "foo": 4
+ },
+ [
+ 5
+ ],
+ {
+ "foo": 5
+ },
+ 4
+ ],
+ "result": [
+ [
+ 4
+ ],
+ {
+ "foo": 4
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, multi-value nodelist",
+ "selector": "$[?value(@.*)==4]",
+ "document": [
+ [
+ 4,
+ 4
+ ],
+ {
+ "foo": 4,
+ "bar": 4
+ }
+ ],
+ "result": [],
+ "result_paths": [],
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, too few params",
+ "selector": "$[?value()==4]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, too many params",
+ "selector": "$[?value(@.a,@.b)==4]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "functions, value, result must be compared",
+ "selector": "$[?value(@.a)]",
+ "invalid_selector": true,
+ "tags": [
+ "function",
+ "value"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between question mark and expression",
+ "selector": "$[? @.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between question mark and expression",
+ "selector": "$[?\n@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between question mark and expression",
+ "selector": "$[?\t@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between question mark and expression",
+ "selector": "$[?\r@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between question mark and parenthesized expression",
+ "selector": "$[? (@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between question mark and parenthesized expression",
+ "selector": "$[?\n(@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between question mark and parenthesized expression",
+ "selector": "$[?\t(@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between question mark and parenthesized expression",
+ "selector": "$[?\r(@.a)]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between parenthesized expression and bracket",
+ "selector": "$[?(@.a) ]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between parenthesized expression and bracket",
+ "selector": "$[?(@.a)\n]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between parenthesized expression and bracket",
+ "selector": "$[?(@.a)\t]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between parenthesized expression and bracket",
+ "selector": "$[?(@.a)\r]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, space between bracket and question mark",
+ "selector": "$[ ?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, newline between bracket and question mark",
+ "selector": "$[\n?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, tab between bracket and question mark",
+ "selector": "$[\t?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, filter, return between bracket and question mark",
+ "selector": "$[\r?@.a]",
+ "document": [
+ {
+ "a": "b",
+ "d": "e"
+ },
+ {
+ "b": "c",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "b",
+ "d": "e"
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between function name and parenthesis",
+ "selector": "$[?count (@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between function name and parenthesis",
+ "selector": "$[?count\n(@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between function name and parenthesis",
+ "selector": "$[?count\t(@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between function name and parenthesis",
+ "selector": "$[?count\r(@.*)==1]",
+ "invalid_selector": true,
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between parenthesis and arg",
+ "selector": "$[?count( @.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between parenthesis and arg",
+ "selector": "$[?count(\n@.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between parenthesis and arg",
+ "selector": "$[?count(\t@.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between parenthesis and arg",
+ "selector": "$[?count(\r@.*)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between arg and comma",
+ "selector": "$[?search(@ ,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between arg and comma",
+ "selector": "$[?search(@\n,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between arg and comma",
+ "selector": "$[?search(@\t,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between arg and comma",
+ "selector": "$[?search(@\r,'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between comma and arg",
+ "selector": "$[?search(@, '[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between comma and arg",
+ "selector": "$[?search(@,\n'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between comma and arg",
+ "selector": "$[?search(@,\t'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between comma and arg",
+ "selector": "$[?search(@,\r'[a-z]+')]",
+ "document": [
+ "foo",
+ "123"
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, space between arg and parenthesis",
+ "selector": "$[?count(@.* )==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "function",
+ "search",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newline between arg and parenthesis",
+ "selector": "$[?count(@.*\n)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tab between arg and parenthesis",
+ "selector": "$[?count(@.*\t)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, return between arg and parenthesis",
+ "selector": "$[?count(@.*\r)==1]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "count",
+ "function",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, spaces in a relative singular selector",
+ "selector": "$[?length(@ .a .b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newlines in a relative singular selector",
+ "selector": "$[?length(@\n.a\n.b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tabs in a relative singular selector",
+ "selector": "$[?length(@\t.a\t.b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, returns in a relative singular selector",
+ "selector": "$[?length(@\r.a\r.b) == 3]",
+ "document": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ },
+ {}
+ ],
+ "result": [
+ {
+ "a": {
+ "b": "foo"
+ }
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, spaces in an absolute singular selector",
+ "selector": "$..[?length(@)==length($ [0] .a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, newlines in an absolute singular selector",
+ "selector": "$..[?length(@)==length($\n[0]\n.a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, tabs in an absolute singular selector",
+ "selector": "$..[?length(@)==length($\t[0]\t.a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, functions, returns in an absolute singular selector",
+ "selector": "$..[?length(@)==length($\r[0]\r.a)]",
+ "document": [
+ {
+ "a": "foo"
+ },
+ {}
+ ],
+ "result": [
+ "foo"
+ ],
+ "result_paths": [
+ "$[0]['a']"
+ ],
+ "tags": [
+ "function",
+ "length",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before ||",
+ "selector": "$[?@.a ||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before ||",
+ "selector": "$[?@.a\n||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before ||",
+ "selector": "$[?@.a\t||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before ||",
+ "selector": "$[?@.a\r||@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after ||",
+ "selector": "$[?@.a|| @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after ||",
+ "selector": "$[?@.a||\n@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after ||",
+ "selector": "$[?@.a||\t@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after ||",
+ "selector": "$[?@.a||\r@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "c": 3
+ }
+ ],
+ "result": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before &&",
+ "selector": "$[?@.a &&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before &&",
+ "selector": "$[?@.a\n&&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before &&",
+ "selector": "$[?@.a\t&&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before &&",
+ "selector": "$[?@.a\r&&@.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after &&",
+ "selector": "$[?@.a&& @.b]",
+ "document": [
+ {
+ "a": 1
+ },
+ {
+ "b": 2
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before ==",
+ "selector": "$[?@.a ==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before ==",
+ "selector": "$[?@.a\n==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before ==",
+ "selector": "$[?@.a\t==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before ==",
+ "selector": "$[?@.a\r==@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after ==",
+ "selector": "$[?@.a== @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after ==",
+ "selector": "$[?@.a==\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after ==",
+ "selector": "$[?@.a==\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after ==",
+ "selector": "$[?@.a==\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ }
+ ],
+ "result_paths": [
+ "$[0]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before !=",
+ "selector": "$[?@.a !=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before !=",
+ "selector": "$[?@.a\n!=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before !=",
+ "selector": "$[?@.a\t!=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before !=",
+ "selector": "$[?@.a\r!=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after !=",
+ "selector": "$[?@.a!= @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after !=",
+ "selector": "$[?@.a!=\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after !=",
+ "selector": "$[?@.a!=\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after !=",
+ "selector": "$[?@.a!=\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before <",
+ "selector": "$[?@.a <@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before <",
+ "selector": "$[?@.a\n<@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before <",
+ "selector": "$[?@.a\t<@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before <",
+ "selector": "$[?@.a\r<@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after <",
+ "selector": "$[?@.a< @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after <",
+ "selector": "$[?@.a<\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after <",
+ "selector": "$[?@.a<\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after <",
+ "selector": "$[?@.a<\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before >",
+ "selector": "$[?@.b >@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before >",
+ "selector": "$[?@.b\n>@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before >",
+ "selector": "$[?@.b\t>@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before >",
+ "selector": "$[?@.b\r>@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after >",
+ "selector": "$[?@.b> @.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after >",
+ "selector": "$[?@.b>\n@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after >",
+ "selector": "$[?@.b>\t@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after >",
+ "selector": "$[?@.b>\r@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before <=",
+ "selector": "$[?@.a <=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before <=",
+ "selector": "$[?@.a\n<=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before <=",
+ "selector": "$[?@.a\t<=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before <=",
+ "selector": "$[?@.a\r<=@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after <=",
+ "selector": "$[?@.a<= @.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after <=",
+ "selector": "$[?@.a<=\n@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after <=",
+ "selector": "$[?@.a<=\t@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after <=",
+ "selector": "$[?@.a<=\r@.b]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space before >=",
+ "selector": "$[?@.b >=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline before >=",
+ "selector": "$[?@.b\n>=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab before >=",
+ "selector": "$[?@.b\t>=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return before >=",
+ "selector": "$[?@.b\r>=@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space after >=",
+ "selector": "$[?@.b>= @.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline after >=",
+ "selector": "$[?@.b>=\n@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab after >=",
+ "selector": "$[?@.b>=\t@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return after >=",
+ "selector": "$[?@.b>=\r@.a]",
+ "document": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ },
+ {
+ "a": 2,
+ "b": 1
+ }
+ ],
+ "result": [
+ {
+ "a": 1,
+ "b": 1
+ },
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space between logical not and test expression",
+ "selector": "$[?! @.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline between logical not and test expression",
+ "selector": "$[?!\n@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab between logical not and test expression",
+ "selector": "$[?!\t@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return between logical not and test expression",
+ "selector": "$[?!\r@.a]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[1]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, space between logical not and parenthesized expression",
+ "selector": "$[?! (@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, newline between logical not and parenthesized expression",
+ "selector": "$[?!\n(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, tab between logical not and parenthesized expression",
+ "selector": "$[?!\t(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, operators, return between logical not and parenthesized expression",
+ "selector": "$[?!\r(@.a=='b')]",
+ "document": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "b",
+ "d": "f"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result": [
+ {
+ "a": "a",
+ "d": "e"
+ },
+ {
+ "a": "d",
+ "d": "f"
+ }
+ ],
+ "result_paths": [
+ "$[0]",
+ "$[2]"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between root and bracket",
+ "selector": "$ ['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between root and bracket",
+ "selector": "$\n['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between root and bracket",
+ "selector": "$\t['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between root and bracket",
+ "selector": "$\r['a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between bracket and bracket",
+ "selector": "$['a'] ['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between bracket and bracket",
+ "selector": "$['a'] \n['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between bracket and bracket",
+ "selector": "$['a'] \t['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between bracket and bracket",
+ "selector": "$['a'] \r['b']",
+ "document": {
+ "a": {
+ "b": "ab"
+ }
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between root and dot",
+ "selector": "$ .a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between root and dot",
+ "selector": "$\n.a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between root and dot",
+ "selector": "$\t.a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between root and dot",
+ "selector": "$\r.a",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between dot and name",
+ "selector": "$. a",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between dot and name",
+ "selector": "$.\na",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between dot and name",
+ "selector": "$.\ta",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between dot and name",
+ "selector": "$.\ra",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between recursive descent and name",
+ "selector": "$.. a",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between recursive descent and name",
+ "selector": "$..\na",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between recursive descent and name",
+ "selector": "$..\ta",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between recursive descent and name",
+ "selector": "$..\ra",
+ "invalid_selector": true,
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between bracket and selector",
+ "selector": "$[ 'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between bracket and selector",
+ "selector": "$[\n'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between bracket and selector",
+ "selector": "$[\t'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between bracket and selector",
+ "selector": "$[\r'a']",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between selector and bracket",
+ "selector": "$['a' ]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between selector and bracket",
+ "selector": "$['a'\n]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between selector and bracket",
+ "selector": "$['a'\t]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between selector and bracket",
+ "selector": "$['a'\r]",
+ "document": {
+ "a": "ab"
+ },
+ "result": [
+ "ab"
+ ],
+ "result_paths": [
+ "$['a']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between selector and comma",
+ "selector": "$['a' ,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between selector and comma",
+ "selector": "$['a'\n,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between selector and comma",
+ "selector": "$['a'\t,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between selector and comma",
+ "selector": "$['a'\r,'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, space between comma and selector",
+ "selector": "$['a', 'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, newline between comma and selector",
+ "selector": "$['a',\n'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, tab between comma and selector",
+ "selector": "$['a',\t'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, selectors, return between comma and selector",
+ "selector": "$['a',\r'b']",
+ "document": {
+ "a": "ab",
+ "b": "bc"
+ },
+ "result": [
+ "ab",
+ "bc"
+ ],
+ "result_paths": [
+ "$['a']",
+ "$['b']"
+ ],
+ "tags": [
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between start and colon",
+ "selector": "$[1 :5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between start and colon",
+ "selector": "$[1\n:5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between start and colon",
+ "selector": "$[1\t:5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between start and colon",
+ "selector": "$[1\r:5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between colon and end",
+ "selector": "$[1: 5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between colon and end",
+ "selector": "$[1:\n5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between colon and end",
+ "selector": "$[1:\t5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between colon and end",
+ "selector": "$[1:\r5:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between end and colon",
+ "selector": "$[1:5 :2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between end and colon",
+ "selector": "$[1:5\n:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between end and colon",
+ "selector": "$[1:5\t:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between end and colon",
+ "selector": "$[1:5\r:2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, space between colon and step",
+ "selector": "$[1:5: 2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, newline between colon and step",
+ "selector": "$[1:5:\n2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, tab between colon and step",
+ "selector": "$[1:5:\t2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ },
+ {
+ "name": "whitespace, slice, return between colon and step",
+ "selector": "$[1:5:\r2]",
+ "document": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ],
+ "result": [
+ 2,
+ 4
+ ],
+ "result_paths": [
+ "$[1]",
+ "$[3]"
+ ],
+ "tags": [
+ "index",
+ "whitespace"
+ ]
+ }
+ ]
+}
diff --git a/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php b/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
index 6871a56511890..1d1eb4be3b431 100644
--- a/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
@@ -49,6 +49,19 @@ public function testAllAuthors()
], $result);
}
+ public function testAllAuthorsWithBrackets()
+ {
+ $result = self::getBookstoreCrawler()->find('$..["author"]');
+
+ $this->assertCount(4, $result);
+ $this->assertSame([
+ 'Nigel Rees',
+ 'Evelyn Waugh',
+ 'Herman Melville',
+ 'J. R. R. Tolkien',
+ ], $result);
+ }
+
public function testAllThingsInStore()
{
$result = self::getBookstoreCrawler()->find('$.store.*');
@@ -58,6 +71,15 @@ public function testAllThingsInStore()
$this->assertArrayHasKey('color', $result[1]);
}
+ public function testAllThingsInStoreWithBrackets()
+ {
+ $result = self::getBookstoreCrawler()->find('$["store"][*]');
+
+ $this->assertCount(2, $result);
+ $this->assertCount(4, $result[0]);
+ $this->assertArrayHasKey('color', $result[1]);
+ }
+
public function testEscapedDoubleQuotesInFieldName()
{
$crawler = new JsonCrawler(<<assertSame(42, $result[0]);
}
+ public function testMultipleKeysAtOnce()
+ {
+ $crawler = new JsonCrawler(<<find("$['a', 'b', 3]");
+
+ $this->assertSame([
+ ['b"c' => 42],
+ ['c' => 43],
+ ], $result);
+ }
+
+ public function testMultipleKeysAtOnceOnArray()
+ {
+ $crawler = new JsonCrawler(<<find("$[0, 2, 'a,b,c', -1]");
+
+ $this->assertCount(4, $result);
+ $this->assertSame(['a' => 1], $result[0]);
+ $this->assertSame(['c' => 3], $result[1]);
+ $this->assertSame(['a,b,c' => 5], $result[2]);
+ $this->assertSame(['d' => 4], $result[3]);
+ }
+
public function testBasicNameSelector()
{
$result = self::getBookstoreCrawler()->find('$.store.book')[0];
@@ -77,6 +128,14 @@ public function testBasicNameSelector()
$this->assertSame('Nigel Rees', $result[0]['author']);
}
+ public function testBasicNameSelectorWithBrackts()
+ {
+ $result = self::getBookstoreCrawler()->find('$["store"]["book"]')[0];
+
+ $this->assertCount(4, $result);
+ $this->assertSame('Nigel Rees', $result[0]['author']);
+ }
+
public function testAllPrices()
{
$result = self::getBookstoreCrawler()->find('$.store..price');
@@ -121,6 +180,25 @@ public function testBooksWithIsbn()
], [$result[0]['isbn'], $result[1]['isbn']]);
}
+ public function testBooksWithPublisherAddress()
+ {
+ $result = self::getBookstoreCrawler()->find('$..book[?(@.publisher.address)]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testBooksWithBracketsAndFilter()
+ {
+ $result = self::getBookstoreCrawler()->find('$..["book"][?(@.isbn)]');
+
+ $this->assertCount(2, $result);
+ $this->assertSame([
+ '0-553-21311-3',
+ '0-395-19395-8',
+ ], [$result[0]['isbn'], $result[1]['isbn']]);
+ }
+
public function testBooksLessThanTenDollars()
{
$result = self::getBookstoreCrawler()->find('$..book[?(@.price < 10)]');
@@ -216,6 +294,14 @@ public function testEverySecondElementReverseSlice()
$this->assertSame([6, 2, 5], $result);
}
+ public function testEverySecondElementReverseSliceAndBrackets()
+ {
+ $crawler = self::getSimpleCollectionCrawler();
+
+ $result = $crawler->find('$["a"][::-2]');
+ $this->assertSame([6, 2, 5], $result);
+ }
+
public function testEmptyResults()
{
$crawler = self::getSimpleCollectionCrawler();
@@ -344,6 +430,50 @@ public function testValueFunction()
$this->assertSame('Sayings of the Century', $result[0]['title']);
}
+ public function testDeepExpressionInFilter()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.address.city == "Springfield")]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testWildcardInFilter()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.* == "my-publisher")]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testWildcardInFunction()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.book[?match(@.publisher.*.city, "Spring.+")]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame('Sword of Honour', $result[0]['title']);
+ }
+
+ public function testUseAtSymbolReturnsAll()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@ == @)]');
+
+ $this->assertSame([
+ 'red',
+ 399,
+ ], $result);
+ }
+
+ public function testUseAtSymbolAloneReturnsAll()
+ {
+ $result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@)]');
+
+ $this->assertSame([
+ 'red',
+ 399,
+ ], $result);
+ }
+
public function testValueFunctionWithOuterParentheses()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?(value(@.price) == 8.95)]');
@@ -370,6 +500,28 @@ public function testLengthFunctionWithOuterParentheses()
$this->assertSame('J. R. R. Tolkien', $result[1]['author']);
}
+ public function testMatchFunctionWithMultipleSpacesTrimmed()
+ {
+ $result = self::getBookstoreCrawler()->find("$.store.book[?(match(@.title, 'Sword of Honour'))]");
+
+ $this->assertSame([], $result);
+ }
+
+ public function testFilterMultiline()
+ {
+ $result = self::getBookstoreCrawler()->find(
+ '$
+ .store
+ .book[?
+ length(@.author)>12
+ ]'
+ );
+
+ $this->assertCount(2, $result);
+ $this->assertSame('Herman Melville', $result[0]['author']);
+ $this->assertSame('J. R. R. Tolkien', $result[1]['author']);
+ }
+
public function testCountFunction()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?count(@.extra) != 0]');
@@ -404,6 +556,260 @@ public function testAcceptsJsonPath()
$this->assertSame('red', $result[0]['color']);
}
+ public function testStarAsKey()
+ {
+ $crawler = new JsonCrawler(<<find('$["*"]');
+
+ $this->assertCount(1, $result);
+ $this->assertSame(['a' => 1, 'b' => 2], $result[0]);
+ }
+
+ /**
+ * @dataProvider provideUnicodeEscapeSequencesProvider
+ */
+ public function testUnicodeEscapeSequences(string $jsonPath, array $expected)
+ {
+ $this->assertSame($expected, self::getUnicodeDocumentCrawler()->find($jsonPath));
+ }
+
+ public static function provideUnicodeEscapeSequencesProvider(): array
+ {
+ return [
+ [
+ '$["caf\u00e9"]',
+ ['coffee'],
+ ],
+ [
+ '$["\u65e5\u672c"]',
+ ['Japan'],
+ ],
+ [
+ '$["M\u00fcller"]',
+ [],
+ ],
+ [
+ '$["emoji\ud83d\ude00"]',
+ ['smiley'],
+ ],
+ [
+ '$["tab\there"]',
+ ['with tab'],
+ ],
+ [
+ '$["quote\"here"]',
+ ['with quote'],
+ ],
+ [
+ '$["backslash\\\\here"]',
+ ['with backslash'],
+ ],
+ [
+ '$["apostrophe\'here"]',
+ ['with apostrophe'],
+ ],
+ [
+ '$["control\u0001char"]',
+ ['with control char'],
+ ],
+ [
+ '$["\u0063af\u00e9"]',
+ ['coffee'],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSingleQuotedStringProvider
+ */
+ public function testSingleQuotedStrings(string $jsonPath, array $expected)
+ {
+ $this->assertSame($expected, self::getUnicodeDocumentCrawler()->find($jsonPath));
+ }
+
+ public static function provideSingleQuotedStringProvider(): array
+ {
+ return [
+ [
+ "$['caf\\u00e9']",
+ ['coffee'],
+ ],
+ [
+ "$['\\u65e5\\u672c']",
+ ['Japan'],
+ ],
+ [
+ "$['quote\"here']",
+ ['with quote'],
+ ],
+ [
+ "$['M\\u00fcller']",
+ [],
+ ],
+ [
+ "$['emoji\\ud83d\\ude00']",
+ ['smiley'],
+ ],
+ [
+ "$['tab\\there']",
+ ['with tab'],
+ ],
+ [
+ "$['quote\\\"here']",
+ ['with quote'],
+ ],
+ [
+ "$['backslash\\\\here']",
+ ['with backslash'],
+ ],
+ [
+ "$['apostrophe\\'here']",
+ ['with apostrophe'],
+ ],
+ [
+ "$['control\\u0001char']",
+ ['with control char'],
+ ],
+ [
+ "$['\\u0063af\\u00e9']",
+ ['coffee'],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideFilterWithUnicodeProvider
+ */
+ public function testFilterWithUnicodeStrings(string $jsonPath, int $expectedCount, string $expectedCountry)
+ {
+ $result = self::getUnicodeDocumentCrawler()->find($jsonPath);
+
+ $this->assertCount($expectedCount, $result);
+
+ if ($expectedCount > 0) {
+ $this->assertSame($expectedCountry, $result[0]['country']);
+ }
+ }
+
+ public static function provideFilterWithUnicodeProvider(): array
+ {
+ return [
+ [
+ '$.users[?(@.name == "caf\u00e9")]',
+ 1,
+ 'France',
+ ],
+ [
+ '$.users[?(@.name == "\u65e5\u672c\u592a\u90ce")]',
+ 1,
+ 'Japan',
+ ],
+ [
+ '$.users[?(@.name == "Jos\u00e9")]',
+ 1,
+ 'Spain',
+ ],
+ [
+ '$.users[?(@.name == "John")]',
+ 1,
+ 'USA',
+ ],
+ [
+ '$.users[?(@.name == "NonExistent\u0020Name")]',
+ 0,
+ '',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInvalidUnicodeSequenceProvider
+ */
+ public function testInvalidUnicodeSequencesAreProcessedAsLiterals(string $jsonPath)
+ {
+ $this->assertIsArray(self::getUnicodeDocumentCrawler()->find($jsonPath), 'invalid unicode sequence should be treated as literal and not throw');
+ }
+
+ public static function provideInvalidUnicodeSequenceProvider(): array
+ {
+ return [
+ [
+ '$["test\uZZZZ"]',
+ ],
+ [
+ '$["test\u123"]',
+ ],
+ [
+ '$["test\u"]',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideComplexUnicodePath
+ */
+ public function testComplexUnicodePaths(string $jsonPath, array $expected)
+ {
+ $complexJson = [
+ 'データ' => [
+ 'ユーザー' => [
+ ['名前' => 'テスト', 'ID' => 1],
+ ['名前' => 'サンプル', 'ID' => 2],
+ ],
+ ],
+ 'special🔑' => [
+ 'value💎' => 'treasure',
+ ],
+ ];
+
+ $crawler = new JsonCrawler(json_encode($complexJson));
+
+ $this->assertSame($expected, $crawler->find($jsonPath));
+ }
+
+ public static function provideComplexUnicodePath(): array
+ {
+ return [
+ [
+ '$["\u30c7\u30fc\u30bf"]["\u30e6\u30fc\u30b6\u30fc"][0]["\u540d\u524d"]',
+ ['テスト'],
+ ],
+ [
+ '$["special\ud83d\udd11"]["value\ud83d\udc8e"]',
+ ['treasure'],
+ ],
+ [
+ '$["\u30c7\u30fc\u30bf"]["\u30e6\u30fc\u30b6\u30fc"][*]["\u540d\u524d"]',
+ ['テスト', 'サンプル'],
+ ],
+ ];
+ }
+
+ public function testSurrogatePairHandling()
+ {
+ $json = ['𝒽𝑒𝓁𝓁𝑜' => 'mathematical script hello'];
+ $crawler = new JsonCrawler(json_encode($json));
+
+ // mathematical script "hello" requires surrogate pairs for each character
+ $result = $crawler->find('$["\ud835\udcbd\ud835\udc52\ud835\udcc1\ud835\udcc1\ud835\udc5c"]');
+ $this->assertSame(['mathematical script hello'], $result);
+ }
+
+ public function testMixedQuoteTypes()
+ {
+ $json = ['key"with"quotes' => 'value1', "key'with'apostrophes" => 'value2'];
+ $crawler = new JsonCrawler(json_encode($json));
+
+ $result = $crawler->find('$[\'key"with"quotes\']');
+ $this->assertSame(['value1'], $result);
+
+ $result = $crawler->find('$["key\'with\'apostrophes"]');
+ $this->assertSame(['value2'], $result);
+ }
+
private static function getBookstoreCrawler(): JsonCrawler
{
return new JsonCrawler(<< 'coffee',
+ '日本' => 'Japan',
+ 'emoji😀' => 'smiley',
+ 'tab here' => 'with tab',
+ "new\nline" => 'with newline',
+ 'quote"here' => 'with quote',
+ 'backslash\\here' => 'with backslash',
+ 'apostrophe\'here' => 'with apostrophe',
+ "control\x01char" => 'with control char',
+ 'users' => [
+ ['name' => 'café', 'country' => 'France'],
+ ['name' => '日本太郎', 'country' => 'Japan'],
+ ['name' => 'John', 'country' => 'USA'],
+ ['name' => 'Müller', 'country' => 'Germany'],
+ ['name' => 'José', 'country' => 'Spain'],
+ ],
+ ];
+
+ return new JsonCrawler(json_encode($json));
+ }
}
diff --git a/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php b/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php
new file mode 100644
index 0000000000000..b39b68abcd463
--- /dev/null
+++ b/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php
@@ -0,0 +1,224 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\JsonPath\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\JsonPath\Exception\JsonCrawlerException;
+use Symfony\Component\JsonPath\JsonCrawler;
+
+final class JsonPathComplianceTestSuiteTest extends TestCase
+{
+ private const UNSUPPORTED_TEST_CASES = [
+ 'basic, multiple selectors, name and index, object data',
+ 'basic, multiple selectors, index and slice',
+ 'basic, multiple selectors, index and slice, overlapping',
+ 'basic, multiple selectors, wildcard and index',
+ 'basic, multiple selectors, wildcard and name',
+ 'basic, multiple selectors, wildcard and slice',
+ 'basic, multiple selectors, multiple wildcards',
+ 'filter, existence, without segments',
+ 'filter, existence, present with null',
+ 'filter, absolute existence, without segments',
+ 'filter, absolute existence, with segments',
+ 'filter, equals null, absent from data',
+ 'filter, absolute, equals self',
+ 'filter, deep equality, arrays',
+ 'filter, deep equality, objects',
+ 'filter, not-equals string, single quotes',
+ 'filter, not-equals numeric string, single quotes',
+ 'filter, not-equals string, single quotes, different type',
+ 'filter, not-equals string, double quotes',
+ 'filter, not-equals numeric string, double quotes',
+ 'filter, not-equals string, double quotes, different types',
+ 'filter, not-equals null, absent from data',
+ 'filter, less than number',
+ 'filter, less than null',
+ 'filter, less than true',
+ 'filter, less than false',
+ 'filter, less than or equal to true',
+ 'filter, greater than number',
+ 'filter, greater than null',
+ 'filter, greater than true',
+ 'filter, greater than false',
+ 'filter, greater than or equal to string, single quotes',
+ 'filter, greater than or equal to string, double quotes',
+ 'filter, greater than or equal to number',
+ 'filter, greater than or equal to null',
+ 'filter, greater than or equal to true',
+ 'filter, greater than or equal to false',
+ 'filter, exists and not-equals null, absent from data',
+ 'filter, exists and exists, data false',
+ 'filter, exists or exists, data false',
+ 'filter, and',
+ 'filter, or',
+ 'filter, not exists, data null',
+ 'filter, non-singular existence, wildcard',
+ 'filter, non-singular existence, multiple',
+ 'filter, non-singular existence, slice',
+ 'filter, non-singular existence, negated',
+ 'filter, nested',
+ 'filter, name segment on primitive, selects nothing',
+ 'filter, name segment on array, selects nothing',
+ 'filter, index segment on object, selects nothing',
+ 'filter, followed by name selector',
+ 'filter, followed by child segment that selects multiple elements',
+ 'filter, multiple selectors',
+ 'filter, multiple selectors, comparison',
+ 'filter, multiple selectors, overlapping',
+ 'filter, multiple selectors, filter and index',
+ 'filter, multiple selectors, filter and wildcard',
+ 'filter, multiple selectors, filter and slice',
+ 'filter, multiple selectors, comparison filter, index and slice',
+ 'filter, equals number, zero and negative zero',
+ 'filter, equals number, negative zero and zero',
+ 'filter, equals number, with and without decimal fraction',
+ 'filter, equals number, exponent',
+ 'filter, equals number, exponent upper e',
+ 'filter, equals number, positive exponent',
+ 'filter, equals number, negative exponent',
+ 'filter, equals number, exponent 0',
+ 'filter, equals number, exponent -0',
+ 'filter, equals number, exponent +0',
+ 'filter, equals number, exponent leading -0',
+ 'filter, equals number, exponent +00',
+ 'filter, equals number, decimal fraction',
+ 'filter, equals number, decimal fraction, trailing 0',
+ 'filter, equals number, decimal fraction, exponent',
+ 'filter, equals number, decimal fraction, positive exponent',
+ 'filter, equals number, decimal fraction, negative exponent',
+ 'filter, equals, empty node list and empty node list',
+ 'filter, equals, empty node list and special nothing',
+ 'filter, object data',
+ 'filter, and binds more tightly than or',
+ 'filter, left to right evaluation',
+ 'filter, group terms, right',
+ 'name selector, double quotes, escaped reverse solidus',
+ 'name selector, single quotes, escaped reverse solidus',
+ 'slice selector, slice selector with everything omitted, long form',
+ 'slice selector, start, min exact',
+ 'slice selector, start, max exact',
+ 'slice selector, end, min exact',
+ 'slice selector, end, max exact',
+ 'basic, descendant segment, multiple selectors',
+ 'basic, bald descendant segment',
+ 'filter, relative non-singular query, index, equal',
+ 'filter, relative non-singular query, index, not equal',
+ 'filter, relative non-singular query, index, less-or-equal',
+ 'filter, relative non-singular query, name, equal',
+ 'filter, relative non-singular query, name, not equal',
+ 'filter, relative non-singular query, name, less-or-equal',
+ 'filter, relative non-singular query, combined, equal',
+ 'filter, relative non-singular query, combined, not equal',
+ 'filter, relative non-singular query, combined, less-or-equal',
+ 'filter, relative non-singular query, wildcard, equal',
+ 'filter, relative non-singular query, wildcard, not equal',
+ 'filter, relative non-singular query, wildcard, less-or-equal',
+ 'filter, relative non-singular query, slice, equal',
+ 'filter, relative non-singular query, slice, not equal',
+ 'filter, relative non-singular query, slice, less-or-equal',
+ 'filter, absolute non-singular query, index, equal',
+ 'filter, absolute non-singular query, index, not equal',
+ 'filter, absolute non-singular query, index, less-or-equal',
+ 'filter, absolute non-singular query, name, equal',
+ 'filter, absolute non-singular query, name, not equal',
+ 'filter, absolute non-singular query, name, less-or-equal',
+ 'filter, absolute non-singular query, combined, equal',
+ 'filter, absolute non-singular query, combined, not equal',
+ 'filter, absolute non-singular query, combined, less-or-equal',
+ 'filter, absolute non-singular query, wildcard, equal',
+ 'filter, absolute non-singular query, wildcard, not equal',
+ 'filter, absolute non-singular query, wildcard, less-or-equal',
+ 'filter, absolute non-singular query, slice, equal',
+ 'filter, absolute non-singular query, slice, not equal',
+ 'filter, absolute non-singular query, slice, less-or-equal',
+ 'filter, equals, special nothing',
+ 'filter, group terms, left',
+ 'index selector, min exact index - 1',
+ 'index selector, max exact index + 1',
+ 'index selector, overflowing index',
+ 'index selector, leading 0',
+ 'index selector, -0',
+ 'index selector, leading -0',
+ 'name selector, double quotes, escaped line feed',
+ 'name selector, double quotes, invalid escaped single quote',
+ 'name selector, double quotes, question mark escape',
+ 'name selector, double quotes, bell escape',
+ 'name selector, double quotes, vertical tab escape',
+ 'name selector, double quotes, 0 escape',
+ 'name selector, double quotes, x escape',
+ 'name selector, double quotes, n escape',
+ 'name selector, double quotes, unicode escape no hex',
+ 'name selector, double quotes, unicode escape too few hex',
+ 'name selector, double quotes, unicode escape upper u',
+ 'name selector, double quotes, unicode escape upper u long',
+ 'name selector, double quotes, unicode escape plus',
+ 'name selector, double quotes, unicode escape brackets',
+ 'name selector, double quotes, unicode escape brackets long',
+ 'name selector, double quotes, single high surrogate',
+ 'name selector, double quotes, single low surrogate',
+ 'name selector, double quotes, high high surrogate',
+ 'name selector, double quotes, low low surrogate',
+ 'name selector, double quotes, supplementary surrogate',
+ 'name selector, double quotes, surrogate incomplete low',
+ 'name selector, single quotes, escaped backspace',
+ 'name selector, single quotes, escaped line feed',
+ 'name selector, single quotes, invalid escaped double quote',
+ 'slice selector, excessively large from value with negative step',
+ 'slice selector, step, min exact - 1',
+ 'slice selector, step, max exact + 1',
+ 'slice selector, overflowing to value',
+ 'slice selector, underflowing from value',
+ 'slice selector, overflowing from value with negative step',
+ 'slice selector, underflowing to value with negative step',
+ 'slice selector, overflowing step',
+ 'slice selector, underflowing step',
+ 'slice selector, step, leading 0',
+ 'slice selector, step, -0',
+ 'slice selector, step, leading -0',
+ ];
+
+ /**
+ * @dataProvider complianceCaseProvider
+ */
+ public function testComplianceTestCase(string $selector, array $document, array $expectedResults, bool $invalidSelector)
+ {
+ $jsonCrawler = new JsonCrawler(json_encode($document));
+
+ if ($invalidSelector) {
+ $this->expectException(JsonCrawlerException::class);
+ }
+
+ $result = $jsonCrawler->find($selector);
+
+ if (!$invalidSelector) {
+ $this->assertContains($result, $expectedResults);
+ }
+ }
+
+ public static function complianceCaseProvider(): iterable
+ {
+ $data = json_decode(file_get_contents(__DIR__.'/Fixtures/cts.json'), true, flags: \JSON_THROW_ON_ERROR);
+
+ foreach ($data['tests'] as $test) {
+ if (\in_array($test['name'], self::UNSUPPORTED_TEST_CASES, true)) {
+ continue;
+ }
+
+ yield $test['name'] => [
+ $test['selector'],
+ $test['document'] ?? [],
+ isset($test['result']) ? [$test['result']] : ($test['results'] ?? []),
+ $test['invalid_selector'] ?? false,
+ ];
+ }
+ }
+}
diff --git a/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php b/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php
index 52d05bdaeb813..cbe6f20d17c0b 100644
--- a/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/JsonPathTest.php
@@ -23,8 +23,8 @@ public function testBuildPath()
->index(0)
->key('address');
- $this->assertSame('$.users[0].address', (string) $path);
- $this->assertSame('$.users[0].address..city', (string) $path->deepScan()->key('city'));
+ $this->assertSame('$["users"][0]["address"]', (string) $path);
+ $this->assertSame('$["users"][0]["address"]..["city"]', (string) $path->deepScan()->key('city'));
}
public function testBuildWithFilter()
@@ -33,7 +33,7 @@ public function testBuildWithFilter()
$path = $path->key('users')
->filter('@.age > 18');
- $this->assertSame('$.users[?(@.age > 18)]', (string) $path);
+ $this->assertSame('$["users"][?(@.age > 18)]', (string) $path);
}
public function testAll()
@@ -42,7 +42,7 @@ public function testAll()
$path = $path->key('users')
->all();
- $this->assertSame('$.users[*]', (string) $path);
+ $this->assertSame('$["users"][*]', (string) $path);
}
public function testFirst()
@@ -51,7 +51,7 @@ public function testFirst()
$path = $path->key('users')
->first();
- $this->assertSame('$.users[0]', (string) $path);
+ $this->assertSame('$["users"][0]', (string) $path);
}
public function testLast()
@@ -60,6 +60,47 @@ public function testLast()
$path = $path->key('users')
->last();
- $this->assertSame('$.users[-1]', (string) $path);
+ $this->assertSame('$["users"][-1]', (string) $path);
+ }
+
+ /**
+ * @dataProvider provideKeysToEscape
+ */
+ public function testEscapedKey(string $key, string $expectedPath)
+ {
+ $path = new JsonPath();
+ $path = $path->key($key);
+
+ $this->assertSame($expectedPath, (string) $path);
+ }
+
+ public static function provideKeysToEscape(): iterable
+ {
+ yield ['simple_key', '$["simple_key"]'];
+ yield ['key"with"quotes', '$["key\\"with\\"quotes"]'];
+ yield ['path\\backslash', '$["path\\backslash"]'];
+ yield ['mixed\\"case', '$["mixed\\\\\\"case"]'];
+ yield ['unicode_🔑', '$["unicode_🔑"]'];
+ yield ['"quotes_only"', '$["\\"quotes_only\\""]'];
+ yield ['\\\\multiple\\\\backslashes', '$["\\\\\\\\multiple\\\\\\backslashes"]'];
+ yield ["control\x00\x1f\x1echar", '$["control\u0000\u001f\u001echar"]'];
+
+ yield ['key"with\\"mixed', '$["key\\"with\\\\\\"mixed"]'];
+ yield ['\\"complex\\"case\\"', '$["\\\\\\"complex\\\\\\"case\\\\\\""]'];
+ yield ['json_like":{"value":"test"}', '$["json_like\\":{\\"value\\":\\"test\\"}"]'];
+ yield ['C:\\Program Files\\"App Name"', '$["C:\\\\Program Files\\\\\\"App Name\\""]'];
+
+ yield ['key_with_é_accents', '$["key_with_é_accents"]'];
+ yield ['unicode_→_arrows', '$["unicode_→_arrows"]'];
+ yield ['chinese_中文_key', '$["chinese_中文_key"]'];
+
+ yield ['', '$[""]'];
+ yield [' ', '$[" "]'];
+ yield [' spaces ', '$[" spaces "]'];
+ yield ["\t\n\r", '$["\\t\\n\\r"]'];
+ yield ["control\x00char", '$["control\u0000char"]'];
+ yield ["newline\nkey", '$["newline\\nkey"]'];
+ yield ["tab\tkey", '$["tab\\tkey"]'];
+ yield ["carriage\rreturn", '$["carriage\\rreturn"]'];
}
}
diff --git a/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php
index 62d64b53e1e8d..1044e7658672b 100644
--- a/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php
@@ -1,5 +1,14 @@
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
namespace Symfony\Component\JsonPath\Tests\Test;
use PHPUnit\Framework\AssertionFailedError;
diff --git a/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php b/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php
index b6768ff7ac9db..fdbd36d3cbc36 100644
--- a/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php
+++ b/src/Symfony/Component/JsonPath/Tests/Tokenizer/JsonPathTokenizerTest.php
@@ -355,9 +355,7 @@ public static function provideInvalidUtf8PropertyName(): array
'special char first' => ['#test'],
'start with digit' => ['123test'],
'asterisk' => ['test*test'],
- 'space not allowed' => [' test'],
'at sign not allowed' => ['@test'],
- 'start control char' => ["\0test"],
'ending control char' => ["test\xFF\xFA"],
'dash sign' => ['-test'],
];
diff --git a/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php b/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php
index d7c5fe44457e7..e9ca872f223b9 100644
--- a/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php
+++ b/src/Symfony/Component/JsonPath/Tokenizer/JsonPathTokenizer.php
@@ -13,6 +13,7 @@
use Symfony\Component\JsonPath\Exception\InvalidJsonPathException;
use Symfony\Component\JsonPath\JsonPath;
+use Symfony\Component\JsonPath\JsonPathUtils;
/**
* @author Alexandre Daubois
@@ -21,6 +22,9 @@
*/
final class JsonPathTokenizer
{
+ private const RFC9535_WHITESPACE_CHARS = [' ', "\t", "\n", "\r"];
+ private const BARE_LITERAL_REGEX = '(true|false|null|\d+(\.\d+)?([eE][+-]?\d+)?|\'[^\']*\'|"[^"]*")';
+
/**
* @return JsonPathToken[]
*/
@@ -34,6 +38,8 @@ public static function tokenize(JsonPath $query): array
$inQuote = false;
$quoteChar = '';
$filterParenthesisDepth = 0;
+ $filterBracketDepth = 0;
+ $hasContentAfterRoot = false;
$chars = mb_str_split((string) $query);
$length = \count($chars);
@@ -42,14 +48,36 @@ public static function tokenize(JsonPath $query): array
throw new InvalidJsonPathException('empty JSONPath expression.');
}
- if ('$' !== $chars[0]) {
+ $i = self::skipWhitespace($chars, 0, $length);
+ if ($i >= $length || '$' !== $chars[$i]) {
throw new InvalidJsonPathException('expression must start with $.');
}
+ $rootIndex = $i;
+ if ($rootIndex + 1 < $length) {
+ $hasContentAfterRoot = true;
+ }
+
for ($i = 0; $i < $length; ++$i) {
$char = $chars[$i];
$position = $i;
+ if (!$inQuote && !$inBracket && self::isWhitespace($char)) {
+ if ('' !== $current) {
+ $tokens[] = new JsonPathToken(TokenType::Name, $current);
+ $current = '';
+ }
+
+ $nextNonWhitespaceIndex = self::skipWhitespace($chars, $i, $length);
+ if ($nextNonWhitespaceIndex < $length && '[' !== $chars[$nextNonWhitespaceIndex] && '.' !== $chars[$nextNonWhitespaceIndex]) {
+ throw new InvalidJsonPathException('whitespace is not allowed in property names.', $i);
+ }
+
+ $i = $nextNonWhitespaceIndex - 1;
+
+ continue;
+ }
+
if (('"' === $char || "'" === $char) && !$inQuote) {
$inQuote = true;
$quoteChar = $char;
@@ -58,10 +86,32 @@ public static function tokenize(JsonPath $query): array
}
if ($inQuote) {
+ // literal control characters (U+0000 through U+001F) in quoted strings
+ // are not be allowed unless they are part of escape sequences
+ $ord = \ord($char);
+ if ($inBracket) {
+ if ($ord <= 31) {
+ $isEscapedChar = ($i > 0 && '\\' === $chars[$i - 1]);
+
+ if (!$isEscapedChar) {
+ throw new InvalidJsonPathException('control characters are not allowed in quoted strings.', $position);
+ }
+ }
+
+ if ("\n" === $char && $i > 0 && '\\' === $chars[$i - 1]) {
+ throw new InvalidJsonPathException('escaped newlines are not allowed in quoted strings.', $position);
+ }
+
+ if ('u' === $char && $i > 0 && '\\' === $chars[$i - 1]) {
+ self::validateUnicodeEscape($chars, $i, $position);
+ }
+ }
+
$current .= $char;
- if ($char === $quoteChar && '\\' !== $chars[$i - 1]) {
+ if ($char === $quoteChar && (0 === $i || '\\' !== $chars[$i - 1])) {
$inQuote = false;
}
+
if ($i === $length - 1 && $inQuote) {
throw new InvalidJsonPathException('unclosed string literal.', $position);
}
@@ -80,11 +130,22 @@ public static function tokenize(JsonPath $query): array
$inBracket = true;
++$bracketDepth;
+ $i = self::skipWhitespace($chars, $i + 1, $length) - 1; // -1 because loop will increment
+
+ continue;
+ }
+
+ if ('[' === $char && $inFilter) {
+ // inside filter expressions, brackets are part of the filter content
+ ++$filterBracketDepth;
+ $current .= $char;
continue;
}
if (']' === $char) {
- if ($inFilter && $filterParenthesisDepth > 0) {
+ if ($inFilter && $filterBracketDepth > 0) {
+ // inside filter expressions, brackets are part of the filter content
+ --$filterBracketDepth;
$current .= $char;
continue;
}
@@ -94,35 +155,61 @@ public static function tokenize(JsonPath $query): array
}
if (0 === $bracketDepth) {
- if ('' === $current) {
+ if ('' === $current = trim($current)) {
throw new InvalidJsonPathException('empty brackets are not allowed.', $position);
}
+ // validate filter expressions
+ if (str_starts_with($current, '?')) {
+ if ($filterParenthesisDepth > 0) {
+ throw new InvalidJsonPathException('unclosed bracket.', $position);
+ }
+ self::validateFilterExpression($current, $position);
+ }
+
$tokens[] = new JsonPathToken(TokenType::Bracket, $current);
$current = '';
$inBracket = false;
$inFilter = false;
$filterParenthesisDepth = 0;
+ $filterBracketDepth = 0;
continue;
}
}
if ('?' === $char && $inBracket && !$inFilter) {
- if ('' !== $current) {
+ if ('' !== trim($current)) {
throw new InvalidJsonPathException('unexpected characters before filter expression.', $position);
}
+
+ $current = '?';
$inFilter = true;
$filterParenthesisDepth = 0;
+ $filterBracketDepth = 0;
+
+ continue;
}
if ($inFilter) {
if ('(' === $char) {
+ if (preg_match('/\w\s+$/', $current)) {
+ throw new InvalidJsonPathException('whitespace is not allowed between function name and parenthesis.', $position);
+ }
++$filterParenthesisDepth;
} elseif (')' === $char) {
if (--$filterParenthesisDepth < 0) {
throw new InvalidJsonPathException('unmatched closing parenthesis in filter.', $position);
}
}
+ $current .= $char;
+
+ continue;
+ }
+
+ if ($inBracket && self::isWhitespace($char)) {
+ $current .= $char;
+
+ continue;
}
// recursive descent
@@ -158,7 +245,7 @@ public static function tokenize(JsonPath $query): array
throw new InvalidJsonPathException('unclosed string literal.', $length - 1);
}
- if ('' !== $current) {
+ if ('' !== $current = trim($current)) {
// final validation of the whole name
if (!preg_match('/^(?:\*|[a-zA-Z_\x{0080}-\x{D7FF}\x{E000}-\x{10FFFF}][a-zA-Z0-9_\x{0080}-\x{D7FF}\x{E000}-\x{10FFFF}]*)$/u', $current)) {
throw new InvalidJsonPathException(\sprintf('invalid character in property name "%s"', $current));
@@ -167,6 +254,230 @@ public static function tokenize(JsonPath $query): array
$tokens[] = new JsonPathToken(TokenType::Name, $current);
}
+ if ($hasContentAfterRoot && !$tokens) {
+ throw new InvalidJsonPathException('invalid JSONPath expression.');
+ }
+
return $tokens;
}
+
+ private static function isWhitespace(string $char): bool
+ {
+ return \in_array($char, self::RFC9535_WHITESPACE_CHARS, true);
+ }
+
+ private static function skipWhitespace(array $chars, int $index, int $length): int
+ {
+ while ($index < $length && self::isWhitespace($chars[$index])) {
+ ++$index;
+ }
+
+ return $index;
+ }
+
+ private static function validateFilterExpression(string $expr, int $position): void
+ {
+ self::validateBareLiterals($expr, $position);
+
+ $filterExpr = ltrim($expr, '?');
+ $filterExpr = trim($filterExpr);
+
+ $comparisonOps = ['==', '!=', '>=', '<=', '>', '<'];
+ foreach ($comparisonOps as $op) {
+ if (str_contains($filterExpr, $op)) {
+ [$left, $right] = array_map('trim', explode($op, $filterExpr, 2));
+
+ // check if either side contains non-singular queries
+ if (self::isNonSingularQuery($left) || self::isNonSingularQuery($right)) {
+ throw new InvalidJsonPathException('Non-singular query is not comparable.', $position);
+ }
+
+ break;
+ }
+ }
+
+ // look for invalid number formats in filter expressions
+ $operators = [...$comparisonOps, '&&', '||'];
+ $tokens = [$filterExpr];
+
+ foreach ($operators as $op) {
+ $newTokens = [];
+ foreach ($tokens as $token) {
+ $newTokens = array_merge($newTokens, explode($op, $token));
+ }
+
+ $tokens = $newTokens;
+ }
+
+ foreach ($tokens as $token) {
+ if (
+ '' === ($token = trim($token))
+ || \in_array($token, ['true', 'false', 'null'], true)
+ || false !== strpbrk($token[0], '@"\'')
+ || false !== strpbrk($token, '()[]$')
+ || (str_contains($token, '.') && !preg_match('/^[\d+\-.eE\s]*\./', $token))
+ ) {
+ continue;
+ }
+
+ // strict JSON number format validation
+ if (
+ preg_match('/^(?=[\d+\-.eE\s]+$)(?=.*\d)/', $token)
+ && !preg_match('/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/', $token)
+ ) {
+ throw new InvalidJsonPathException(\sprintf('Invalid number format "%s" in filter expression.', $token), $position);
+ }
+ }
+ }
+
+ private static function validateBareLiterals(string $expr, int $position): void
+ {
+ $filterExpr = ltrim($expr, '?');
+ $filterExpr = trim($filterExpr);
+
+ if (preg_match('/\b(True|False|Null)\b/', $filterExpr)) {
+ throw new InvalidJsonPathException('Incorrectly capitalized literal in filter expression.', $position);
+ }
+
+ if (preg_match('/^(length|count|value)\s*\([^)]*\)$/', $filterExpr)) {
+ throw new InvalidJsonPathException('Function result must be compared.', $position);
+ }
+
+ if (preg_match('/\b(length|count|value)\s*\(([^)]*)\)/', $filterExpr, $matches)) {
+ $functionName = $matches[1];
+ $args = trim($matches[2]);
+ if (!$args) {
+ throw new InvalidJsonPathException('Function requires exactly one argument.', $position);
+ }
+
+ $argParts = JsonPathUtils::parseCommaSeparatedValues($args);
+ if (1 !== \count($argParts)) {
+ throw new InvalidJsonPathException('Function requires exactly one argument.', $position);
+ }
+
+ $arg = trim($argParts[0]);
+
+ if ('count' === $functionName && preg_match('/^'.self::BARE_LITERAL_REGEX.'$/', $arg)) {
+ throw new InvalidJsonPathException('count() function requires a query argument, not a literal.', $position);
+ }
+
+ if ('length' === $functionName && preg_match('/@\.\*/', $arg)) {
+ throw new InvalidJsonPathException('Function argument must be a singular query.', $position);
+ }
+ }
+
+ if (preg_match('/\b(match|search)\s*\(([^)]*)\)/', $filterExpr, $matches)) {
+ $args = trim($matches[2]);
+ if (!$args) {
+ throw new InvalidJsonPathException('Function requires exactly two arguments.', $position);
+ }
+
+ $argParts = JsonPathUtils::parseCommaSeparatedValues($args);
+ if (2 !== \count($argParts)) {
+ throw new InvalidJsonPathException('Function requires exactly two arguments.', $position);
+ }
+ }
+
+ if (preg_match('/^'.self::BARE_LITERAL_REGEX.'$/', $filterExpr)) {
+ throw new InvalidJsonPathException('Bare literal in filter expression - literals must be compared.', $position);
+ }
+
+ if (preg_match('/\b'.self::BARE_LITERAL_REGEX.'\s*(&&|\|\|)\s*'.self::BARE_LITERAL_REGEX.'\b/', $filterExpr)) {
+ throw new InvalidJsonPathException('Bare literals in logical expression - literals must be compared.', $position);
+ }
+
+ if (preg_match('/\b(match|search|length|count|value)\s*\([^)]*\)\s*[=!]=\s*(true|false)\b/', $filterExpr)
+ || preg_match('/\b(true|false)\s*[=!]=\s*(match|search|length|count|value)\s*\([^)]*\)/', $filterExpr)) {
+ throw new InvalidJsonPathException('Function result cannot be compared to boolean literal.', $position);
+ }
+
+ if (preg_match('/\b'.self::BARE_LITERAL_REGEX.'\s*(&&|\|\|)/', $filterExpr)
+ || preg_match('/(&&|\|\|)\s*'.self::BARE_LITERAL_REGEX.'\b/', $filterExpr)) {
+ // check if the literal is not part of a comparison
+ if (!preg_match('/(@[^=<>!]*|[^=<>!@]+)\s*[=<>!]+\s*'.self::BARE_LITERAL_REGEX.'/', $filterExpr)
+ && !preg_match('/'.self::BARE_LITERAL_REGEX.'\s*[=<>!]+\s*(@[^=<>!]*|[^=<>!@]+)/', $filterExpr)
+ ) {
+ throw new InvalidJsonPathException('Bare literal in logical expression - literals must be compared.', $position);
+ }
+ }
+ }
+
+ private static function isNonSingularQuery(string $query): bool
+ {
+ if (!str_starts_with($query = trim($query), '@')) {
+ return false;
+ }
+
+ if (preg_match('/@(\.\.)|(.*\[\*])|(.*\.\*)|(.*\[.*:.*])|(.*\[.*,.*])/', $query)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static function validateUnicodeEscape(array $chars, int $index, int $position): void
+ {
+ if ($index + 4 >= \count($chars)) {
+ return;
+ }
+
+ $hexDigits = '';
+ for ($i = 1; $i <= 4; ++$i) {
+ $hexDigits .= $chars[$index + $i];
+ }
+
+ if (!preg_match('/^[0-9A-Fa-f]{4}$/', $hexDigits)) {
+ return;
+ }
+
+ $codePoint = hexdec($hexDigits);
+
+ if ($codePoint >= 0xD800 && $codePoint <= 0xDBFF) {
+ $nextIndex = $index + 5;
+
+ if ($nextIndex + 1 < \count($chars)
+ && '\\' === $chars[$nextIndex] && 'u' === $chars[$nextIndex + 1]
+ ) {
+ $nextHexDigits = '';
+ for ($i = 2; $i <= 5; ++$i) {
+ $nextHexDigits .= $chars[$nextIndex + $i];
+ }
+
+ if (preg_match('/^[0-9A-Fa-f]{4}$/', $nextHexDigits)) {
+ $nextCodePoint = hexdec($nextHexDigits);
+
+ // high surrogate must be followed by low surrogate
+ if ($nextCodePoint < 0xDC00 || $nextCodePoint > 0xDFFF) {
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ }
+ } else {
+ // high surrogate not followed by low surrogate
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ } elseif ($codePoint >= 0xDC00 && $codePoint <= 0xDFFF) {
+ $prevIndex = $index - 7; // position of \ in previous \uXXXX (7 positions back: u+4hex+\+u)
+
+ if ($prevIndex >= 0
+ && '\\' === $chars[$prevIndex] && 'u' === $chars[$prevIndex + 1]
+ ) {
+ $prevHexDigits = '';
+ for ($i = 2; $i <= 5; ++$i) {
+ $prevHexDigits .= $chars[$prevIndex + $i];
+ }
+
+ if (preg_match('/^[0-9A-Fa-f]{4}$/', $prevHexDigits)) {
+ $prevCodePoint = hexdec($prevHexDigits);
+
+ // low surrogate must be preceded by high surrogate
+ if ($prevCodePoint < 0xD800 || $prevCodePoint > 0xDBFF) {
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ }
+ } else {
+ // low surrogate not preceded by high surrogate
+ throw new InvalidJsonPathException('Invalid Unicode surrogate pair.', $position);
+ }
+ }
+ }
}
diff --git a/src/Symfony/Component/JsonPath/composer.json b/src/Symfony/Component/JsonPath/composer.json
index fe8ddf84dd82d..feb8158aa5be2 100644
--- a/src/Symfony/Component/JsonPath/composer.json
+++ b/src/Symfony/Component/JsonPath/composer.json
@@ -17,6 +17,7 @@
],
"require": {
"php": ">=8.2",
+ "symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
diff --git a/src/Symfony/Component/JsonStreamer/JsonStreamReader.php b/src/Symfony/Component/JsonStreamer/JsonStreamReader.php
index b2f2fabaa3dad..e813f4a8a5408 100644
--- a/src/Symfony/Component/JsonStreamer/JsonStreamReader.php
+++ b/src/Symfony/Component/JsonStreamer/JsonStreamReader.php
@@ -45,7 +45,7 @@ public function __construct(
private ContainerInterface $valueTransformers,
PropertyMetadataLoaderInterface $propertyMetadataLoader,
string $streamReadersDir,
- string $lazyGhostsDir,
+ ?string $lazyGhostsDir = null,
) {
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir);
$this->instantiator = new Instantiator();
diff --git a/src/Symfony/Component/Ldap/Security/LdapUser.php b/src/Symfony/Component/Ldap/Security/LdapUser.php
index ef73b82422d0b..020fcb5441596 100644
--- a/src/Symfony/Component/Ldap/Security/LdapUser.php
+++ b/src/Symfony/Component/Ldap/Security/LdapUser.php
@@ -47,7 +47,7 @@ public function getRoles(): array
public function getPassword(): ?string
{
- return $this->password;
+ return $this->password ?? null;
}
public function getSalt(): ?string
@@ -89,7 +89,7 @@ public function isEqualTo(UserInterface $user): bool
return false;
}
- if ($this->getPassword() !== $user->getPassword()) {
+ if (($this->getPassword() ?? $user->getPassword()) !== $user->getPassword()) {
return false;
}
diff --git a/src/Symfony/Component/Ldap/Tests/Security/LdapUserTest.php b/src/Symfony/Component/Ldap/Tests/Security/LdapUserTest.php
new file mode 100644
index 0000000000000..0a696bcd0c29d
--- /dev/null
+++ b/src/Symfony/Component/Ldap/Tests/Security/LdapUserTest.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Ldap\Tests\Security;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Ldap\Entry;
+use Symfony\Component\Ldap\Security\LdapUser;
+
+class LdapUserTest extends TestCase
+{
+ public function testIsEqualToWorksOnUnserializedUser()
+ {
+ $user = new LdapUser(new Entry('uid=jonhdoe,ou=MyBusiness,dc=symfony,dc=com', []), 'jonhdoe', 'p455w0rd');
+ $unserializedUser = unserialize(serialize($user));
+
+ $this->assertTrue($unserializedUser->isEqualTo($user));
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
index 84e2553a627cc..e5bfb4daddc2e 100644
--- a/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
+++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Transport/MailerSendSmtpTransport.php
@@ -22,7 +22,7 @@ final class MailerSendSmtpTransport extends EsmtpTransport
{
public function __construct(string $username, #[\SensitiveParameter] string $password, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
- parent::__construct('smtp.mailersend.net', 587, true, $dispatcher, $logger);
+ parent::__construct('smtp.mailersend.net', 587, false, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
index fa34f9abb7caf..08879782a0bc3 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
+++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
@@ -98,9 +98,6 @@ public function testCustomHeader()
$this->assertEquals('amp-html-value', $payload['amp-html']);
}
- /**
- * @legacy
- */
public function testPrefixHeaderWithH()
{
$email = new Email();
diff --git a/src/Symfony/Component/Mailer/Command/MailerTestCommand.php b/src/Symfony/Component/Mailer/Command/MailerTestCommand.php
index 6cde762f5ed8c..8e00f629877c7 100644
--- a/src/Symfony/Component/Mailer/Command/MailerTestCommand.php
+++ b/src/Symfony/Component/Mailer/Command/MailerTestCommand.php
@@ -35,10 +35,10 @@ protected function configure(): void
{
$this
->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message')
- ->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'from@example.org')
- ->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'The subject of the message', 'Testing transport')
- ->addOption('body', null, InputOption::VALUE_OPTIONAL, 'The body of the message', 'Testing body')
- ->addOption('transport', null, InputOption::VALUE_OPTIONAL, 'The transport to be used')
+ ->addOption('from', null, InputOption::VALUE_REQUIRED, 'The sender of the message', 'from@example.org')
+ ->addOption('subject', null, InputOption::VALUE_REQUIRED, 'The subject of the message', 'Testing transport')
+ ->addOption('body', null, InputOption::VALUE_REQUIRED, 'The body of the message', 'Testing body')
+ ->addOption('transport', null, InputOption::VALUE_REQUIRED, 'The transport to be used')
->setHelp(<<<'EOF'
The %command.name% command tests a Mailer transport by sending a simple email message:
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
index a893441b03a9a..5de88e71fa247 100644
--- a/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
+++ b/src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php
@@ -16,6 +16,8 @@
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\Header\Headers;
+use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
@@ -144,6 +146,27 @@ public function testSendOneDeadAndRecoveryWithinRetryPeriod()
$this->assertTransports($t, 1, []);
}
+ public function testSendOneDeadMessageAlterationsDoNotPersist()
+ {
+ $t1 = $this->createMock(TransportInterface::class);
+ $t1->expects($this->once())->method('send')
+ ->willReturnCallback(function (Message $message) {
+ $message->getHeaders()->addTextHeader('X-Transport-1', 'value');
+ throw new TransportException();
+ });
+ $t2 = $this->createMock(TransportInterface::class);
+ $t2->expects($this->once())->method('send');
+ $t = new RoundRobinTransport([$t1, $t2]);
+ $p = new \ReflectionProperty($t, 'cursor');
+ $p->setValue($t, 0);
+ $headers = new Headers();
+ $headers->addTextHeader('X-Shared', 'value');
+ $message = new Message($headers);
+ $t->send($message);
+ $this->assertSame($message->getHeaders()->get('X-Shared')->getBody(), 'value');
+ $this->assertFalse($message->getHeaders()->has('X-Transport-1'));
+ }
+
public function testFailureDebugInformation()
{
$t1 = $this->createMock(TransportInterface::class);
diff --git a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
index 4925b40d0bb6a..e48644f790b56 100644
--- a/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php
@@ -50,7 +50,7 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess
while ($transport = $this->getNextTransport()) {
try {
- return $transport->send($message, $envelope);
+ return $transport->send(clone $message, $envelope);
} catch (TransportExceptionInterface $e) {
$exception ??= new TransportException('All transports failed.');
$exception->appendDebug(\sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
index 1a84381008318..9fcb6a5e17e80 100644
--- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
+++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php
@@ -313,7 +313,7 @@ private function convertToBytes(string $memoryLimit): int
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
- $max = (int) $max;
+ $max = (float) $max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
@@ -326,6 +326,6 @@ private function convertToBytes(string $memoryLimit): int
case 'k': $max *= 1024;
}
- return $max;
+ return (int) $max;
}
}
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
index de2b6f3f14d12..e86765cca1407 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php
@@ -35,7 +35,7 @@ protected function configure(): void
new InputArgument('id', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Remove all failed messages from the transport'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'),
- new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
+ new InputOption('transport', null, InputOption::VALUE_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'),
new InputOption('class-filter', null, InputOption::VALUE_REQUIRED, 'Filter by a specific class name'),
])
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
index 15dbe84a37da3..32f535703cebe 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php
@@ -65,8 +65,8 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::IS_ARRAY, 'Specific message id(s) to retry'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force action without confirmation'),
- new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
- new InputOption('keepalive', null, InputOption::VALUE_OPTIONAL, 'Whether to use the transport\'s keepalive mechanism if implemented', self::DEFAULT_KEEPALIVE_INTERVAL),
+ new InputOption('transport', null, InputOption::VALUE_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
+ new InputOption('keepalive', null, InputOption::VALUE_REQUIRED, 'Whether to use the transport\'s keepalive mechanism if implemented', self::DEFAULT_KEEPALIVE_INTERVAL),
])
->setHelp(<<<'EOF'
The %command.name% retries message in the failure transport.
diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
index f052f86bf92c2..927e6705a0e94 100644
--- a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
+++ b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php
@@ -35,7 +35,7 @@ protected function configure(): void
->setDefinition([
new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'),
new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50),
- new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
+ new InputOption('transport', null, InputOption::VALUE_REQUIRED, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
new InputOption('stats', null, InputOption::VALUE_NONE, 'Display the message count by class'),
new InputOption('class-filter', null, InputOption::VALUE_REQUIRED, 'Filter by a specific class name'),
])
diff --git a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
index 7790e074ad609..7183f2e7c67d7 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\Messenger\Tests\Command;
use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerTrait;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Tester\CommandCompletionTester;
@@ -205,6 +207,48 @@ public function testRunWithTimeLimit()
$this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay());
}
+ public function testRunWithMemoryLimit()
+ {
+ $envelope = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]);
+
+ $receiver = $this->createMock(ReceiverInterface::class);
+ $receiver->method('get')->willReturn([$envelope]);
+
+ $receiverLocator = new Container();
+ $receiverLocator->set('dummy-receiver', $receiver);
+
+ $bus = $this->createMock(MessageBusInterface::class);
+
+ $busLocator = new Container();
+ $busLocator->set('dummy-bus', $bus);
+
+ $logger = new class() implements LoggerInterface {
+ use LoggerTrait;
+
+ public array $logs = [];
+
+ public function log(...$args): void
+ {
+ $this->logs[] = $args;
+ }
+ };
+ $command = new ConsumeMessagesCommand(new RoutableMessageBus($busLocator), $receiverLocator, new EventDispatcher(), $logger);
+
+ $application = new Application();
+ $application->add($command);
+ $tester = new CommandTester($application->get('messenger:consume'));
+ $tester->execute([
+ 'receivers' => ['dummy-receiver'],
+ '--memory-limit' => '1.5M',
+ ]);
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $this->assertStringContainsString('[OK] Consuming messages from transport "dummy-receiver"', $tester->getDisplay());
+ $this->assertStringContainsString('The worker will automatically exit once it has exceeded 1.5M of memory', $tester->getDisplay());
+
+ $this->assertSame(1572864, $logger->logs[1][2]['limit']);
+ }
+
public function testRunWithAllOption()
{
$envelope1 = new Envelope(new \stdClass(), [new BusNameStamp('dummy-bus')]);
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
index 67af3ac9237a7..65f48bcd7ac19 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
@@ -75,13 +75,13 @@ protected function doSend(MessageInterface $message): SentMessage
$options['from'] = $message->getFrom() ?: $this->from;
$options['source'] ??= $this->source;
$options['list_id'] ??= $this->listId;
- $options['from_email'] ?? $this->fromEmail;
+ $options['from_email'] ??= $this->fromEmail;
if (isset($options['from']) && !preg_match('/^[a-zA-Z0-9\s]{3,11}$/', $options['from']) && !preg_match('/^\+[1-9]\d{1,14}$/', $options['from'])) {
throw new InvalidArgumentException(\sprintf('The "From" number "%s" is not a valid phone number, shortcode, or alphanumeric sender ID.', $options['from']));
}
- if ($options['list_id'] ?? false) {
+ if (!$options['list_id']) {
$options['to'] = $message->getPhone();
}
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
index e1f9fa37dcae0..532c5aceba3aa 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
@@ -24,7 +24,7 @@
final class ClickSendTransportTest extends TransportTestCase
{
- public static function createTransport(?HttpClientInterface $client = null, string $from = 'test_from', string $source = 'test_source', int $listId = 99, string $fromEmail = 'foo@bar.com'): ClickSendTransport
+ public static function createTransport(?HttpClientInterface $client = null, ?string $from = 'test_from', ?string $source = 'test_source', ?int $listId = 99, ?string $fromEmail = 'foo@bar.com'): ClickSendTransport
{
return new ClickSendTransport('test_username', 'test_key', $from, $source, $listId, $fromEmail, $client ?? new MockHttpClient());
}
@@ -70,6 +70,10 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$body = json_decode($options['body'], true);
self::assertIsArray($body);
self::assertArrayHasKey('messages', $body);
+ $message = reset($body['messages']);
+ self::assertArrayHasKey('from_email', $message);
+ self::assertArrayHasKey('list_id', $message);
+ self::assertArrayNotHasKey('to', $message);
return $response;
});
@@ -77,6 +81,29 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$transport->send($message);
}
+ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValidWithoutOptionalParameters()
+ {
+ $message = new SmsMessage('+33612345678', 'Hello!');
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects(self::exactly(2))->method('getStatusCode')->willReturn(200);
+ $response->expects(self::once())->method('getContent')->willReturn('');
+ $client = new MockHttpClient(function (string $method, string $url, array $options) use ($response): ResponseInterface {
+ self::assertSame('POST', $method);
+ self::assertSame('https://rest.clicksend.com/v3/sms/send', $url);
+
+ $body = json_decode($options['body'], true);
+ self::assertIsArray($body);
+ self::assertArrayHasKey('messages', $body);
+ $message = reset($body['messages']);
+ self::assertArrayNotHasKey('list_id', $message);
+ self::assertArrayHasKey('to', $message);
+
+ return $response;
+ });
+ $transport = $this->createTransport($client, null, null, null, null);
+ $transport->send($message);
+ }
+
public static function toStringProvider(): iterable
{
yield ['clicksend://rest.clicksend.com?from=test_from&source=test_source&list_id=99&from_email=foo%40bar.com', self::createTransport()];
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
index e652e879ed64f..e182e66fb848a 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsEmailTransport.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\Notifier\Bridge\FakeSms;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
@@ -20,6 +19,7 @@
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
index ca90a24a449d9..7a37386875816 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsLoggerTransport.php
@@ -12,12 +12,12 @@
namespace Symfony\Component\Notifier\Bridge\FakeSms;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException;
use Symfony\Component\Notifier\Message\MessageInterface;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Transport\AbstractTransport;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
diff --git a/src/Symfony/Component/ObjectMapper/ObjectMapper.php b/src/Symfony/Component/ObjectMapper/ObjectMapper.php
index d78bc3ce8d216..69f02fb7f1160 100644
--- a/src/Symfony/Component/ObjectMapper/ObjectMapper.php
+++ b/src/Symfony/Component/ObjectMapper/ObjectMapper.php
@@ -70,7 +70,7 @@ public function map(object $source, object|string|null $target = null): object
$mappedTarget = $mappingToObject ? $target : $targetRefl->newInstanceWithoutConstructor();
if ($map && $map->transform) {
- $mappedTarget = $this->applyTransforms($map, $mappedTarget, $mappedTarget, null);
+ $mappedTarget = $this->applyTransforms($map, $mappedTarget, $source, null);
if (!\is_object($mappedTarget)) {
throw new MappingTransformException(\sprintf('Cannot map "%s" to a non-object target of type "%s".', get_debug_type($source), get_debug_type($mappedTarget)));
diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/A.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/A.php
new file mode 100644
index 0000000000000..77ab0c3a3a76e
--- /dev/null
+++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/A.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments;
+
+use Symfony\Component\ObjectMapper\Attribute\Map;
+
+#[Map(target: B::class, transform: [B::class, 'newInstance'])]
+class A
+{
+}
diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/B.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/B.php
new file mode 100644
index 0000000000000..b5ea60066b59f
--- /dev/null
+++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/InstanceCallbackWithArguments/B.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments;
+
+class B
+{
+ public mixed $transformValue;
+ public object $transformSource;
+
+ public static function newInstance(mixed $value, object $source): self
+ {
+ $b = new self();
+ $b->transformValue = $value;
+ $b->transformSource = $source;
+
+ return $b;
+ }
+}
diff --git a/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php b/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
index a416abd47933b..99153c3fbdfc7 100644
--- a/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
+++ b/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
@@ -34,6 +34,8 @@
use Symfony\Component\ObjectMapper\Tests\Fixtures\HydrateObject\SourceOnly;
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallback\A as InstanceCallbackA;
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallback\B as InstanceCallbackB;
+use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments\A as InstanceCallbackWithArgumentsA;
+use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments\B as InstanceCallbackWithArgumentsB;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\AToBMapper;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\MapStructMapperMetadataFactory;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\Source;
@@ -155,6 +157,16 @@ public function testMapToWithInstanceHook()
$this->assertSame($b->name, 'test');
}
+ public function testMapToWithInstanceHookWithArguments()
+ {
+ $a = new InstanceCallbackWithArgumentsA();
+ $mapper = new ObjectMapper();
+ $b = $mapper->map($a);
+ $this->assertInstanceOf(InstanceCallbackWithArgumentsB::class, $b);
+ $this->assertSame($a, $b->transformSource);
+ $this->assertInstanceOf(InstanceCallbackWithArgumentsB::class, $b->transformValue);
+ }
+
public function testMapStruct()
{
$a = new Source('a', 'b', 'c');
@@ -284,11 +296,11 @@ public function testMultipleTargetMapProperty()
$mapper = new ObjectMapper();
$b = $mapper->map($u, MultipleTargetPropertyB::class);
$this->assertInstanceOf(MultipleTargetPropertyB::class, $b);
- $this->assertEquals($b->foo, 'TEST');
+ $this->assertEquals('TEST', $b->foo);
$c = $mapper->map($u, MultipleTargetPropertyC::class);
$this->assertInstanceOf(MultipleTargetPropertyC::class, $c);
- $this->assertEquals($c->bar, 'test');
- $this->assertEquals($c->foo, 'donotmap');
- $this->assertEquals($c->doesNotExistInTargetB, 'foo');
+ $this->assertEquals('test', $c->bar);
+ $this->assertEquals('donotmap', $c->foo);
+ $this->assertEquals('foo', $c->doesNotExistInTargetB);
}
}
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
index d34e19f0c9b19..7066e1545e7d6 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
@@ -109,11 +109,6 @@ public function getValue(object|array $objectOrArray, string|PropertyPathInterfa
return $propertyValues[\count($propertyValues) - 1][self::VALUE];
}
- /**
- * @template T of object|array
- * @param T $objectOrArray
- * @param-out ($objectOrArray is array ? array : T) $objectOrArray
- */
public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void
{
if (\is_object($objectOrArray) && (false === strpbrk((string) $propertyPath, '.[') || $objectOrArray instanceof \stdClass && property_exists($objectOrArray, $propertyPath))) {
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
index 2e25e9e517db2..ccbaf8b3c4b49 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
@@ -39,6 +39,12 @@ interface PropertyAccessorInterface
*
* If neither is found, an exception is thrown.
*
+ * @template T of object|array
+ *
+ * @param T $objectOrArray
+ *
+ * @param-out ($objectOrArray is array ? array : T) $objectOrArray
+ *
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
index a7d36203d49c6..9a4924f9338dd 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
@@ -973,7 +973,7 @@ public static function unionTypesProvider(): iterable
Type::object(ParentDummy::class),
Type::null(),
)];
- yield ['f', null];
+ yield ['f', Type::union(Type::string(), Type::null())];
yield ['g', Type::array(Type::union(Type::string(), Type::int()))];
}
diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json
index f8ae018a40b7f..f8714b7f3fbc3 100644
--- a/src/Symfony/Component/PropertyInfo/composer.json
+++ b/src/Symfony/Component/PropertyInfo/composer.json
@@ -26,7 +26,7 @@
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/string": "^6.4|^7.0",
- "symfony/type-info": "~7.1.9|^7.2.2"
+ "symfony/type-info": "~7.2.8|^7.3.1"
},
"require-dev": {
"symfony/serializer": "^6.4|^7.0",
diff --git a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
index a252814570f2e..c0c290e686800 100644
--- a/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
+++ b/src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
@@ -20,7 +20,7 @@ class BasicErrorHandler
{
public static function register(bool $debug): void
{
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', $debug);
diff --git a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
index 0dfc7de0ca7a0..47c67605b0430 100644
--- a/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
+++ b/src/Symfony/Component/Runtime/Internal/SymfonyErrorHandler.php
@@ -30,7 +30,7 @@ public static function register(bool $debug): void
return;
}
- error_reporting(-1);
+ error_reporting(\E_ALL & ~\E_DEPRECATED & ~\E_USER_DEPRECATED);
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
ini_set('display_errors', $debug);
diff --git a/src/Symfony/Component/Runtime/SymfonyRuntime.php b/src/Symfony/Component/Runtime/SymfonyRuntime.php
index 4035f28c806cd..4667bbdfba24f 100644
--- a/src/Symfony/Component/Runtime/SymfonyRuntime.php
+++ b/src/Symfony/Component/Runtime/SymfonyRuntime.php
@@ -162,7 +162,11 @@ public function getRunner(?object $application): RunnerInterface
if (!$application->getName() || !$console->has($application->getName())) {
$application->setName($_SERVER['argv'][0]);
- $console->add($application);
+ if (method_exists($console, 'addCommand')) {
+ $console->addCommand($application);
+ } else {
+ $console->add($application);
+ }
}
$console->setDefaultCommand($application->getName(), true);
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/application.php b/src/Symfony/Component/Runtime/Tests/phpt/application.php
index ca2de555edfb7..b51947c2afaf1 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/application.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/application.php
@@ -25,7 +25,11 @@
});
$app = new Application();
- $app->add($command);
+ if (method_exists($app, 'addCommand')) {
+ $app->addCommand($command);
+ } else {
+ $app->add($command);
+ }
$app->setDefaultCommand('go', true);
return $app;
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
index 929b4401e86b9..aa40eda627151 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command_list.php
@@ -23,7 +23,11 @@
$command->setName('my_command');
[$cmd, $args] = $runtime->getResolver(require __DIR__.'/command.php')->resolve();
- $app->add($cmd(...$args));
+ if (method_exists($app, 'addCommand')) {
+ $app->addCommand($cmd(...$args));
+ } else {
+ $app->add($cmd(...$args));
+ }
return $app;
};
diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
index b2e18a29efe51..683e46d4e0eb8 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
@@ -32,16 +32,12 @@ abstract class AbstractToken implements TokenInterface, \Serializable
*/
public function __construct(array $roles = [])
{
- $this->roleNames = [];
-
- foreach ($roles as $role) {
- $this->roleNames[] = (string) $role;
- }
+ $this->roleNames = $roles;
}
public function getRoleNames(): array
{
- return $this->roleNames ??= self::__construct($this->user->getRoles()) ?? $this->roleNames;
+ return $this->roleNames ??= $this->user?->getRoles() ?? [];
}
public function getUserIdentifier(): string
@@ -90,13 +86,7 @@ public function eraseCredentials(): void
*/
public function __serialize(): array
{
- $data = [$this->user, true, null, $this->attributes];
-
- if (!$this->user instanceof EquatableInterface) {
- $data[] = $this->roleNames;
- }
-
- return $data;
+ return [$this->user, true, null, $this->attributes, $this->getRoleNames()];
}
/**
@@ -160,12 +150,7 @@ public function __toString(): string
$class = static::class;
$class = substr($class, strrpos($class, '\\') + 1);
- $roles = [];
- foreach ($this->roleNames as $role) {
- $roles[] = $role;
- }
-
- return \sprintf('%s(user="%s", roles="%s")', $class, $this->getUserIdentifier(), implode(', ', $roles));
+ return \sprintf('%s(user="%s", roles="%s")', $class, $this->getUserIdentifier(), implode(', ', $this->getRoleNames()));
}
/**
diff --git a/src/Symfony/Component/Security/Core/User/UserInterface.php b/src/Symfony/Component/Security/Core/User/UserInterface.php
index 120521211b326..24c0581f83cbd 100644
--- a/src/Symfony/Component/Security/Core/User/UserInterface.php
+++ b/src/Symfony/Component/Security/Core/User/UserInterface.php
@@ -15,9 +15,7 @@
* Represents the interface that all user classes must implement.
*
* This interface is useful because the authentication layer can deal with
- * the object through its lifecycle, using the object to get the hashed
- * password (for checking against a submitted password), assigning roles
- * and so on.
+ * the object through its lifecycle, assigning roles and so on.
*
* Regardless of how your users are loaded or where they come from (a database,
* configuration, web service, etc.), you will have a class that implements
diff --git a/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php b/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php
index e6741a48f1945..b92be87e6ef89 100644
--- a/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php
+++ b/src/Symfony/Component/Security/Core/Validator/Constraints/UserPassword.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Security\Core\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
@@ -25,6 +26,7 @@ class UserPassword extends Constraint
public string $message = 'This value should be the user\'s current password.';
public string $service = 'security.validator.user_password';
+ #[HasNamedArguments]
public function __construct(?array $options = null, ?string $message = null, ?string $service = null, ?array $groups = null, mixed $payload = null)
{
parent::__construct($options, $groups, $payload);
diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php
index 6c256dba60955..da616e86ccc99 100644
--- a/src/Symfony/Component/Security/Http/Firewall.php
+++ b/src/Symfony/Component/Security/Http/Firewall.php
@@ -122,7 +122,11 @@ public static function getSubscribedEvents()
protected function callListeners(RequestEvent $event, iterable $listeners)
{
foreach ($listeners as $listener) {
- $listener($event);
+ if (!$listener instanceof FirewallListenerInterface) {
+ $listener($event);
+ } elseif (false !== $listener->supports($event->getRequest())) {
+ $listener->authenticate($event);
+ }
if ($event->hasResponse()) {
break;
diff --git a/src/Symfony/Component/Security/Http/FirewallMap.php b/src/Symfony/Component/Security/Http/FirewallMap.php
index 3b01cbdc161a6..444f71ceebbda 100644
--- a/src/Symfony/Component/Security/Http/FirewallMap.php
+++ b/src/Symfony/Component/Security/Http/FirewallMap.php
@@ -14,6 +14,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -25,12 +26,12 @@
class FirewallMap implements FirewallMapInterface
{
/**
- * @var list, ExceptionListener|null, LogoutListener|null}>
+ * @var list, ExceptionListener|null, LogoutListener|null}>
*/
private array $map = [];
/**
- * @param list $listeners
+ * @param list $listeners
*/
public function add(?RequestMatcherInterface $requestMatcher = null, array $listeners = [], ?ExceptionListener $exceptionListener = null, ?LogoutListener $logoutListener = null): void
{
diff --git a/src/Symfony/Component/Security/Http/FirewallMapInterface.php b/src/Symfony/Component/Security/Http/FirewallMapInterface.php
index fa43d6a6e9ba3..1925d3dec23a0 100644
--- a/src/Symfony/Component/Security/Http/FirewallMapInterface.php
+++ b/src/Symfony/Component/Security/Http/FirewallMapInterface.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
@@ -35,7 +36,7 @@ interface FirewallMapInterface
* If there is no logout listener, the third element of the outer array
* must be null.
*
- * @return array{iterable, ExceptionListener, LogoutListener}
+ * @return array{iterable, ExceptionListener, LogoutListener}
*/
public function getListeners(Request $request): array;
}
diff --git a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
index f9417d237433c..89040f3875f2b 100644
--- a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php
@@ -18,7 +18,9 @@
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Http\Firewall;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
+use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
class FirewallTest extends TestCase
@@ -97,4 +99,59 @@ public function testOnKernelRequestWithSubRequest()
$this->assertFalse($event->hasResponse());
}
+
+ public function testListenersAreCalled()
+ {
+ $calledListeners = [];
+
+ $callableListener = static function() use(&$calledListeners) { $calledListeners[] = 'callableListener'; };
+ $firewallListener = new class($calledListeners) implements FirewallListenerInterface {
+ public function __construct(private array &$calledListeners) {}
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ $this->calledListeners[] = 'firewallListener';
+ }
+
+ public static function getPriority(): int
+ {
+ return 0;
+ }
+ };
+ $callableFirewallListener = new class($calledListeners) extends AbstractListener {
+ public function __construct(private array &$calledListeners) {}
+
+ public function supports(Request $request): ?bool
+ {
+ return true;
+ }
+
+ public function authenticate(RequestEvent $event): void
+ {
+ $this->calledListeners[] = 'callableFirewallListener';
+ }
+ };
+
+ $request = $this->createMock(Request::class);
+
+ $map = $this->createMock(FirewallMapInterface::class);
+ $map
+ ->expects($this->once())
+ ->method('getListeners')
+ ->with($this->equalTo($request))
+ ->willReturn([[$callableListener, $firewallListener, $callableFirewallListener], null, null])
+ ;
+
+ $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
+
+ $firewall = new Firewall($map, $this->createMock(EventDispatcherInterface::class));
+ $firewall->onKernelRequest($event);
+
+ $this->assertSame(['callableListener', 'firewallListener', 'callableFirewallListener'], $calledListeners);
+ }
}
diff --git a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
index bd4f505f8acf9..60cb2ec495b67 100644
--- a/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
+++ b/src/Symfony/Component/Serializer/Debug/TraceableSerializer.php
@@ -171,8 +171,8 @@ private function getCaller(string $method, string $interface): array
&& $method === $trace[$i]['function']
&& is_a($trace[$i]['class'], $interface, true)
) {
- $file = $trace[$i]['file'];
- $line = $trace[$i]['line'];
+ $file = $trace[$i]['file'] ?? $trace[$i + 1]['file'];
+ $line = $trace[$i]['line'] ?? $trace[$i + 1]['line'];
break;
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
index c346aafa8f450..d9a50fef0cbd2 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
@@ -26,6 +26,7 @@
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
+use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -1093,6 +1094,30 @@ protected function createChildContext(array $parentContext, string $attribute, ?
return $context;
}
+ protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
+ {
+ if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
+ return false;
+ }
+
+ if (null !== $this->classDiscriminatorResolver) {
+ $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
+ if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
+ $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
+ }
+
+ if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
+ $attributes = [];
+ foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
+ $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
+ }
+ $allowedAttributes = array_merge($allowedAttributes, ...$attributes);
+ }
+ }
+
+ return $allowedAttributes;
+ }
+
/**
* Builds the cache key for the attributes cache.
*
diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
index 1d60cba50b0c3..cbba35ba0f674 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
@@ -20,7 +20,6 @@
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Exception\LogicException;
-use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -149,30 +148,6 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v
}
}
- protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
- {
- if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
- return false;
- }
-
- if (null !== $this->classDiscriminatorResolver) {
- $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
- if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
- $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
- }
-
- if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
- $attributes = [];
- foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
- $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
- }
- $allowedAttributes = array_merge($allowedAttributes, ...$attributes);
- }
- }
-
- return $allowedAttributes;
- }
-
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php
index f95f2d72e0b46..7308bfc7c754a 100644
--- a/src/Symfony/Component/Serializer/Serializer.php
+++ b/src/Symfony/Component/Serializer/Serializer.php
@@ -213,7 +213,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
throw new NotNormalizableValueException(\sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type));
}
- if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) {
+ if ((isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) && !isset($context['not_normalizable_value_exceptions'])) {
unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]);
$context['not_normalizable_value_exceptions'] = [];
$errors = &$context['not_normalizable_value_exceptions'];
diff --git a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
index 9092433214abf..fa3a4117618ea 100644
--- a/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Debug/TraceableSerializerTest.php
@@ -128,6 +128,40 @@ public function testAddDebugTraceIdInContext()
$traceableSerializer->encode('data', 'format');
$traceableSerializer->decode('data', 'format');
}
+
+ public function testCollectedCaller()
+ {
+ $serializer = new \Symfony\Component\Serializer\Serializer();
+
+ $collector = new SerializerDataCollector();
+ $traceableSerializer = new TraceableSerializer($serializer, $collector);
+
+ $traceableSerializer->normalize('data');
+ $collector->lateCollect();
+
+ $this->assertSame([
+ 'name' => 'TraceableSerializerTest.php',
+ 'file' => __FILE__,
+ 'line' => __LINE__ - 6,
+ ], $collector->getData()['normalize'][0]['caller']);
+ }
+
+ public function testCollectedCallerFromArrayMap()
+ {
+ $serializer = new \Symfony\Component\Serializer\Serializer();
+
+ $collector = new SerializerDataCollector();
+ $traceableSerializer = new TraceableSerializer($serializer, $collector);
+
+ array_map([$traceableSerializer, 'normalize'], ['data']);
+ $collector->lateCollect();
+
+ $this->assertSame([
+ 'name' => 'TraceableSerializerTest.php',
+ 'file' => __FILE__,
+ 'line' => __LINE__ - 6,
+ ], $collector->getData()['normalize'][0]['caller']);
+ }
}
class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
index 31206ea67d289..ea26589a2b072 100644
--- a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php
@@ -20,6 +20,7 @@
'one' => DummyMessageNumberOne::class,
'two' => DummyMessageNumberTwo::class,
'three' => DummyMessageNumberThree::class,
+ 'four' => DummyMessageNumberFour::class,
])]
interface DummyMessageInterface
{
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php
new file mode 100644
index 0000000000000..eaf87d48a7101
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Fixtures;
+
+use Symfony\Component\Serializer\Attribute\Ignore;
+
+abstract class SomeAbstract {
+ #[Ignore]
+ public function getDescription()
+ {
+ return 'Hello, World!';
+ }
+}
+
+class DummyMessageNumberFour extends SomeAbstract implements DummyMessageInterface
+{
+ public function __construct(public $one)
+ {
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
index 7068b8c8e6f49..50b9e2a83c26c 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php
@@ -45,6 +45,7 @@
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
+use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
@@ -52,6 +53,8 @@
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberFour;
use Symfony\Component\Serializer\Tests\Fixtures\DummySecondChildQuux;
use Symfony\Component\Serializer\Tests\Fixtures\DummyString;
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable;
@@ -1235,6 +1238,25 @@ public static function provideBooleanTypesData()
];
}
+ public function testDeserializeAndSerializeConstructorAndIgnoreAndInterfacedObjectsWithTheClassMetadataDiscriminator()
+ {
+ $example = new DummyMessageNumberFour('Hello');
+
+ $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
+
+ $normalizer = new PropertyNormalizer(
+ $classMetadataFactory,
+ null,
+ new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]),
+ new ClassDiscriminatorFromClassMetadata($classMetadataFactory),
+ );
+
+ $serialized = $normalizer->normalize($example, 'json');
+ $deserialized = $normalizer->denormalize($serialized, DummyMessageInterface::class, 'json');
+
+ $this->assertEquals($example, $deserialized);
+ }
+
/**
* @dataProvider provideDenormalizeWithFilterBoolData
*/
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
index 439dce056995c..66033f6bc8efd 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
@@ -747,6 +747,8 @@ public function testDoesntHaveIssuesWithUnionConstTypes()
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
$this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class {
+ public const TEST = 'me';
+
/** @var self::*|null */
public $foo;
})::class)->foo);
diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
index 16b88a32d0442..d6502f8adacd7 100644
--- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
@@ -1714,6 +1714,54 @@ public function testCollectDenormalizationErrorsDefaultContext()
$serializer->denormalize($data, DummyWithVariadicParameter::class);
}
+
+ public function testDenormalizationFailsWithMultipleErrorsInDefaultContext()
+ {
+ $serializer = new Serializer(
+ [new DateTimeNormalizer(), new ObjectNormalizer()],
+ [],
+ [DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true]
+ );
+
+ $data = ['date' => '', 'unknown' => null];
+
+ try {
+ $serializer->denormalize($data, DummyEntityWithStringAndDateTime::class);
+ $this->fail('Expected PartialDenormalizationException was not thrown');
+ } catch (PartialDenormalizationException $e) {
+ $this->assertIsArray($e->getErrors());
+ $this->assertCount(2, $e->getErrors(), 'Expected two denormalization errors');
+
+ $exceptionsAsArray = array_map(function (NotNormalizableValueException $ex): array {
+ return [
+ 'currentType' => $ex->getCurrentType(),
+ 'expectedTypes' => $ex->getExpectedTypes(),
+ 'path' => $ex->getPath(),
+ 'useMessageForUser' => $ex->canUseMessageForUser(),
+ 'message' => $ex->getMessage(),
+ ];
+ }, $e->getErrors());
+
+ $expected = [
+ [
+ 'currentType' => 'null',
+ 'expectedTypes' => ['string'],
+ 'path' => 'bar',
+ 'useMessageForUser' => true,
+ 'message' => 'Failed to create object because the class misses the "bar" property.',
+ ],
+ [
+ 'currentType' => 'string',
+ 'expectedTypes' => ['string'],
+ 'path' => 'date',
+ 'useMessageForUser' => true,
+ 'message' => 'The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.',
+ ],
+ ];
+
+ $this->assertSame($expected, $exceptionsAsArray);
+ }
+ }
}
class Model
@@ -1780,6 +1828,15 @@ public function __construct($value)
}
}
+class DummyEntityWithStringAndDateTime
+{
+ public function __construct(
+ public string $bar,
+ public \DateTimeInterface $date,
+ ) {
+ }
+}
+
class DummyUnionType
{
/**
diff --git a/src/Symfony/Component/Translation/Command/TranslationPullCommand.php b/src/Symfony/Component/Translation/Command/TranslationPullCommand.php
index ad42716f55583..3324bd56e2e35 100644
--- a/src/Symfony/Component/Translation/Command/TranslationPullCommand.php
+++ b/src/Symfony/Component/Translation/Command/TranslationPullCommand.php
@@ -84,10 +84,10 @@ protected function configure(): void
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
- new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
- new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
- new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
- new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
+ new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
+ new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format.', 'xlf12'),
+ new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The %command.name%> command pulls translations from the given provider. Only
diff --git a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php
index b1cdc5fc0b87c..7208ca25fe8de 100644
--- a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php
+++ b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php
@@ -77,8 +77,8 @@ protected function configure(): void
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
- new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
- new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
+ new InputOption('domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
+ new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The %command.name%> command pushes translations to the given provider. Only new
diff --git a/src/Symfony/Component/TypeInfo/.gitattributes b/src/Symfony/Component/TypeInfo/.gitattributes
index 14c3c35940427..413aef4cac05d 100644
--- a/src/Symfony/Component/TypeInfo/.gitattributes
+++ b/src/Symfony/Component/TypeInfo/.gitattributes
@@ -1,3 +1,4 @@
/Tests export-ignore
+/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php text eol=crlf
/phpunit.xml.dist export-ignore
/.git* export-ignore
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithConstants.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithConstants.php
new file mode 100644
index 0000000000000..c849fd59b504d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithConstants.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Fixtures;
+
+final class DummyWithConstants
+{
+ public const DUMMY_STRING_A = 'a';
+ public const DUMMY_INT_A = 1;
+ public const DUMMY_FLOAT_A = 1.23;
+ public const DUMMY_TRUE_A = true;
+ public const DUMMY_FALSE_A = false;
+ public const DUMMY_NULL_A = null;
+ public const DUMMY_ARRAY_A = [];
+ public const DUMMY_ENUM_A = DummyEnum::ONE;
+
+ public const DUMMY_MIX_1 = self::DUMMY_STRING_A;
+ public const DUMMY_MIX_2 = self::DUMMY_INT_A;
+ public const DUMMY_MIX_3 = self::DUMMY_FLOAT_A;
+ public const DUMMY_MIX_4 = self::DUMMY_TRUE_A;
+ public const DUMMY_MIX_5 = self::DUMMY_FALSE_A;
+ public const DUMMY_MIX_6 = self::DUMMY_NULL_A;
+ public const DUMMY_MIX_7 = self::DUMMY_ARRAY_A;
+ public const DUMMY_MIX_8 = self::DUMMY_ENUM_A;
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php
index 0b65137e4cdda..7f73190df1549 100644
--- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php
@@ -12,11 +12,15 @@
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
/**
+ * @phpstan-type CustomArray = array{0: CustomInt, 1: CustomString, 2: bool}
* @phpstan-type CustomString = string
+ *
* @phpstan-import-type CustomInt from DummyWithPhpDoc
* @phpstan-import-type CustomInt from DummyWithPhpDoc as AliasedCustomInt
*
+ * @psalm-type PsalmCustomArray = array{0: PsalmCustomInt, 1: PsalmCustomString, 2: bool}
* @psalm-type PsalmCustomString = string
+ *
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc as PsalmAliasedCustomInt
*/
@@ -53,9 +57,31 @@ final class DummyWithTypeAliases
public mixed $psalmOtherAliasedExternalAlias;
}
+/**
+ * @phpstan-type Foo = array{0: Bar}
+ * @phpstan-type Bar = array{0: Foo}
+ */
+final class DummyWithRecursiveTypeAliases
+{
+}
+
+/**
+ * @phpstan-type Invalid = SomethingInvalid
+ */
+final class DummyWithInvalidTypeAlias
+{
+}
+
/**
* @phpstan-import-type Invalid from DummyWithTypeAliases
*/
final class DummyWithInvalidTypeAliasImport
{
}
+
+/**
+ * @phpstan-import-type Invalid from int
+ */
+final class DummyWithTypeAliasImportedFromInvalidClassName
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php
new file mode 100644
index 0000000000000..9c7d09be76370
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUsesWindowsLineEndings.php
@@ -0,0 +1,22 @@
+createdAt = $createdAt;
+ }
+
+ public function getType(): Type
+ {
+ throw new \LogicException('Should not be called.');
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
index e7794e4f114b6..cf0f1bb91179f 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
@@ -15,10 +15,14 @@
use Symfony\Component\TypeInfo\Exception\LogicException;
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAlias;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAliasImport;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithRecursiveTypeAliases;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliasImportedFromInvalidClassName;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUsesWindowsLineEndings;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
@@ -85,6 +89,24 @@ public function testCollectUses()
$this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithUses::class, 'setCreatedAt'], 'createdAt'))->uses);
}
+ public function testCollectUsesWindowsLineEndings()
+ {
+ self::assertSame(\count(file(__DIR__.'/../Fixtures/DummyWithUsesWindowsLineEndings.php')), substr_count(file_get_contents(__DIR__.'/../Fixtures/DummyWithUsesWindowsLineEndings.php'), "\r\n"));
+
+ $uses = [
+ 'Type' => Type::class,
+ \DateTimeInterface::class => '\\'.\DateTimeInterface::class,
+ 'DateTime' => '\\'.\DateTimeImmutable::class,
+ ];
+
+ $this->assertSame($uses, $this->typeContextFactory->createFromClassName(DummyWithUsesWindowsLineEndings::class)->uses);
+
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithUsesWindowsLineEndings::class))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithUsesWindowsLineEndings::class, 'createdAt'))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithUsesWindowsLineEndings::class, 'setCreatedAt'))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithUsesWindowsLineEndings::class, 'setCreatedAt'], 'createdAt'))->uses);
+ }
+
public function testCollectTemplates()
{
$this->assertEquals([], $this->typeContextFactory->createFromClassName(Dummy::class)->templates);
@@ -128,27 +150,33 @@ public function testCollectTypeAliases()
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
+ 'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
+ 'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
+ 'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
+ 'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases);
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
+ 'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
+ 'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases);
}
@@ -167,4 +195,28 @@ public function testThrowWhenImportingInvalidAlias()
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class);
}
+
+ public function testThrowWhenCannotResolveTypeAlias()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Cannot resolve "Invalid" type alias.');
+
+ $this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAlias::class);
+ }
+
+ public function testThrowWhenTypeAliasNotImportedFromValidClassName()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Type alias "Invalid" is not imported from a valid class name.');
+
+ $this->typeContextFactory->createFromClassName(DummyWithTypeAliasImportedFromInvalidClassName::class);
+ }
+
+ public function testThrowWhenImportingRecursiveTypeAliases()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Cannot resolve "Bar" type alias.');
+
+ $this->typeContextFactory->createFromClassName(DummyWithRecursiveTypeAliases::class)->typeAliases;
+ }
}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
index fcfe909cecf6e..3b194128661c9 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithConstants;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
use Symfony\Component\TypeInfo\Type;
@@ -79,6 +80,7 @@ public static function resolveDataProvider(): iterable
yield [Type::arrayShape(['foo' => Type::bool()], sealed: false), 'array{foo: bool, ...}'];
yield [Type::arrayShape(['foo' => Type::bool()], extraKeyType: Type::int(), extraValueType: Type::string()), 'array{foo: bool, ...}'];
yield [Type::arrayShape(['foo' => Type::bool()], extraValueType: Type::int()), 'array{foo: bool, ...}'];
+ yield [Type::arrayShape(['foo' => Type::union(Type::bool(), Type::float(), Type::int(), Type::null(), Type::string()), 'bar' => Type::string()]), 'array{foo: scalar|null, bar: string}'];
// object shape
yield [Type::object(), 'object{foo: true, bar: false}'];
@@ -95,6 +97,19 @@ public static function resolveDataProvider(): iterable
yield [Type::string(), '"string"'];
yield [Type::true(), 'true'];
+ // const fetch
+ yield [Type::string(), DummyWithConstants::class.'::DUMMY_STRING_*'];
+ yield [Type::string(), DummyWithConstants::class.'::DUMMY_STRING_A'];
+ yield [Type::int(), DummyWithConstants::class.'::DUMMY_INT_*'];
+ yield [Type::int(), DummyWithConstants::class.'::DUMMY_INT_A'];
+ yield [Type::float(), DummyWithConstants::class.'::DUMMY_FLOAT_*'];
+ yield [Type::bool(), DummyWithConstants::class.'::DUMMY_TRUE_*'];
+ yield [Type::bool(), DummyWithConstants::class.'::DUMMY_FALSE_*'];
+ yield [Type::null(), DummyWithConstants::class.'::DUMMY_NULL_*'];
+ yield [Type::array(), DummyWithConstants::class.'::DUMMY_ARRAY_*'];
+ yield [Type::enum(DummyEnum::class, Type::string()), DummyWithConstants::class.'::DUMMY_ENUM_*'];
+ yield [Type::union(Type::string(), Type::int(), Type::float(), Type::bool(), Type::null(), Type::array(), Type::enum(DummyEnum::class, Type::string())), DummyWithConstants::class.'::DUMMY_MIX_*'];
+
// identifiers
yield [Type::bool(), 'bool'];
yield [Type::bool(), 'boolean'];
@@ -157,6 +172,9 @@ public static function resolveDataProvider(): iterable
yield [Type::generic(Type::object(\DateTime::class), Type::string(), Type::bool()), \DateTime::class.''];
yield [Type::generic(Type::object(\DateTime::class), Type::generic(Type::object(\Stringable::class), Type::bool())), \sprintf('%s<%s>', \DateTime::class, \Stringable::class)];
yield [Type::int(), 'int<0, 100>'];
+ yield [Type::string(), \sprintf('value-of<%s>', DummyBackedEnum::class)];
+ yield [Type::int(), 'key-of>'];
+ yield [Type::bool(), 'value-of>'];
// union
yield [Type::union(Type::int(), Type::string()), 'int|string'];
@@ -216,9 +234,21 @@ public function testCannotResolveParentWithoutTypeContext()
$this->resolver->resolve('parent');
}
- public function testCannotUnknownIdentifier()
+ public function testCannotResolveUnknownIdentifier()
{
$this->expectException(UnsupportedException::class);
$this->resolver->resolve('unknown');
}
+
+ public function testCannotResolveKeyOfInvalidType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('key-of');
+ }
+
+ public function testCannotResolveValueOfInvalidType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('value-of');
+ }
}
diff --git a/src/Symfony/Component/TypeInfo/Type/CollectionType.php b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
index 80fbbdba6c3fa..a801f2b51f8d0 100644
--- a/src/Symfony/Component/TypeInfo/Type/CollectionType.php
+++ b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
@@ -65,25 +65,27 @@ public static function mergeCollectionValueTypes(array $types): Type
$boolTypes = [];
$objectTypes = [];
- foreach ($types as $t) {
- // cannot create an union with a standalone type
- if ($t->isIdentifiedBy(TypeIdentifier::MIXED)) {
- return Type::mixed();
- }
+ foreach ($types as $type) {
+ foreach (($type instanceof UnionType ? $type->getTypes() : [$type]) as $t) {
+ // cannot create an union with a standalone type
+ if ($t->isIdentifiedBy(TypeIdentifier::MIXED)) {
+ return Type::mixed();
+ }
- if ($t->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE, TypeIdentifier::BOOL)) {
- $boolTypes[] = $t;
+ if ($t->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE, TypeIdentifier::BOOL)) {
+ $boolTypes[] = $t;
- continue;
- }
+ continue;
+ }
- if ($t->isIdentifiedBy(TypeIdentifier::OBJECT)) {
- $objectTypes[] = $t;
+ if ($t->isIdentifiedBy(TypeIdentifier::OBJECT)) {
+ $objectTypes[] = $t;
- continue;
- }
+ continue;
+ }
- $normalizedTypes[] = $t;
+ $normalizedTypes[] = $t;
+ }
}
$boolTypes = array_unique($boolTypes);
diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
index d268c85fe49b0..8e1cc3d4314e7 100644
--- a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
@@ -123,7 +123,7 @@ private function collectUses(\ReflectionClass $reflection): array
return [];
}
- if (false === $lines = @file($fileName)) {
+ if (false === $lines = @file($fileName, \FILE_IGNORE_NEW_LINES)) {
throw new RuntimeException(\sprintf('Unable to read file "%s".', $fileName));
}
@@ -133,7 +133,7 @@ private function collectUses(\ReflectionClass $reflection): array
foreach ($lines as $line) {
if (str_starts_with($line, 'use ')) {
$inUseSection = true;
- $use = explode(' as ', substr($line, 4, -2), 2);
+ $use = explode(' as ', substr($line, 4, -1), 2);
$alias = 1 === \count($use) ? substr($use[0], false !== ($p = strrpos($use[0], '\\')) ? 1 + $p : 0) : $use[1];
$uses[$alias] = $use[0];
@@ -199,32 +199,85 @@ private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $t
}
$aliases = [];
- foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
- if (!$tag->value instanceof TypeAliasTagValueNode) {
+ $resolvedAliases = [];
+
+ foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
+ if (!$tag->value instanceof TypeAliasImportTagValueNode) {
continue;
}
- $aliases[$tag->value->alias] = $this->stringTypeResolver->resolve((string) $tag->value->type, $typeContext);
+ $importedFromType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
+ if (!$importedFromType instanceof ObjectType) {
+ throw new LogicException(\sprintf('Type alias "%s" is not imported from a valid class name.', $tag->value->importedAlias));
+ }
+
+ $importedFromContext = $this->createFromClassName($importedFromType->getClassName());
+
+ $typeAlias = $importedFromContext->typeAliases[$tag->value->importedAlias] ?? null;
+ if (!$typeAlias) {
+ throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedFromType->getClassName()));
+ }
+
+ $resolvedAliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
}
- foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
- if (!$tag->value instanceof TypeAliasImportTagValueNode) {
+ foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
+ if (!$tag->value instanceof TypeAliasTagValueNode) {
continue;
}
- /** @var ObjectType $importedType */
- $importedType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
- $importedTypeContext = $this->createFromClassName($importedType->getClassName());
+ $aliases[$tag->value->alias] = (string) $tag->value->type;
+ }
- $typeAlias = $importedTypeContext->typeAliases[$tag->value->importedAlias] ?? null;
- if (!$typeAlias) {
- throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedType->getClassName()));
+ return $this->resolveTypeAliases($aliases, $resolvedAliases, $typeContext);
+ }
+
+ /**
+ * @param array $toResolve
+ * @param array $resolved
+ *
+ * @return array
+ */
+ private function resolveTypeAliases(array $toResolve, array $resolved, TypeContext $typeContext): array
+ {
+ if (!$toResolve) {
+ return [];
+ }
+
+ $typeContext = new TypeContext(
+ $typeContext->calledClassName,
+ $typeContext->declaringClassName,
+ $typeContext->namespace,
+ $typeContext->uses,
+ $typeContext->templates,
+ $typeContext->typeAliases + $resolved,
+ );
+
+ $succeeded = false;
+ $lastFailure = null;
+ $lastFailingAlias = null;
+
+ foreach ($toResolve as $alias => $type) {
+ try {
+ $resolved[$alias] = $this->stringTypeResolver->resolve($type, $typeContext);
+ unset($toResolve[$alias]);
+ $succeeded = true;
+ } catch (UnsupportedException $lastFailure) {
+ $lastFailingAlias = $alias;
}
+ }
+
+ // nothing has succeeded, the result won't be different from the
+ // previous one, we can stop here.
+ if (!$succeeded) {
+ throw new LogicException(\sprintf('Cannot resolve "%s" type alias.', $lastFailingAlias), 0, $lastFailure);
+ }
- $aliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
+ if ($toResolve) {
+ return $this->resolveTypeAliases($toResolve, $resolved, $typeContext);
}
- return $aliases;
+ return $resolved;
}
private function getPhpDocNode(string $rawDocNode): PhpDocNode
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
index 475e0212490d7..844de98963e3d 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -18,6 +18,7 @@
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
@@ -38,6 +39,7 @@
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\GenericType;
@@ -131,6 +133,47 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
}
if ($node instanceof ConstTypeNode) {
+ if ($node->constExpr instanceof ConstFetchNode) {
+ $className = match (strtolower($node->constExpr->className)) {
+ 'self' => $typeContext->getDeclaringClass(),
+ 'static' => $typeContext->getCalledClass(),
+ 'parent' => $typeContext->getParentClass(),
+ default => $node->constExpr->className,
+ };
+
+ if (!class_exists($className)) {
+ return Type::mixed();
+ }
+
+ $types = [];
+
+ foreach ((new \ReflectionClass($className))->getReflectionConstants() as $const) {
+ if (preg_match('/^'.str_replace('\*', '.*', preg_quote($node->constExpr->name, '/')).'$/', $const->getName())) {
+ $constValue = $const->getValue();
+
+ $types[] = match (true) {
+ true === $constValue,
+ false === $constValue => Type::bool(),
+ null === $constValue => Type::null(),
+ \is_string($constValue) => Type::string(),
+ \is_int($constValue) => Type::int(),
+ \is_float($constValue) => Type::float(),
+ \is_array($constValue) => Type::array(),
+ $constValue instanceof \UnitEnum => Type::enum($constValue::class),
+ default => Type::mixed(),
+ };
+ }
+ }
+
+ $types = array_unique($types);
+
+ if (\count($types) > 2) {
+ return Type::union(...$types);
+ }
+
+ return $types[0] ?? Type::null();
+ }
+
return match ($node->constExpr::class) {
ConstExprArrayNode::class => Type::array(),
ConstExprFalseNode::class => Type::false(),
@@ -195,6 +238,28 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
}
if ($node instanceof GenericTypeNode) {
+ if ($node->type instanceof IdentifierTypeNode && 'value-of' === $node->type->name) {
+ $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext);
+ if ($type instanceof BackedEnumType) {
+ return $type->getBackingType();
+ }
+
+ if ($type instanceof CollectionType) {
+ return $type->getCollectionValueType();
+ }
+
+ throw new \DomainException(\sprintf('"%s" is not a valid type for "value-of".', $node->genericTypes[0]));
+ }
+
+ if ($node->type instanceof IdentifierTypeNode && 'key-of' === $node->type->name) {
+ $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext);
+ if ($type instanceof CollectionType) {
+ return $type->getCollectionKeyType();
+ }
+
+ throw new \DomainException(\sprintf('"%s" is not a valid type for "key-of".', $node->genericTypes[0]));
+ }
+
$type = $this->getTypeFromNode($node->type, $typeContext);
// handle integer ranges as simple integers
diff --git a/src/Symfony/Component/Uid/UuidV7.php b/src/Symfony/Component/Uid/UuidV7.php
index 0a6f01be1f234..9eff1f247b487 100644
--- a/src/Symfony/Component/Uid/UuidV7.php
+++ b/src/Symfony/Component/Uid/UuidV7.php
@@ -62,7 +62,7 @@ public static function generate(?\DateTimeInterface $time = null): string
if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
randomize:
- self::$rand = unpack('n*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16));
+ self::$rand = unpack('S*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16));
self::$rand[1] &= 0x03FF;
self::$time = $time;
} else {
@@ -78,7 +78,7 @@ public static function generate(?\DateTimeInterface $time = null): string
// 24-bit number in the self::$seedParts list and decrement self::$seedIndex.
if (!self::$seedIndex) {
- $s = unpack('l*', self::$seed = hash('sha512', self::$seed, true));
+ $s = unpack(\PHP_INT_SIZE >= 8 ? 'L*' : 'l*', self::$seed = hash('sha512', self::$seed, true));
$s[] = ($s[1] >> 8 & 0xFF0000) | ($s[2] >> 16 & 0xFF00) | ($s[3] >> 24 & 0xFF);
$s[] = ($s[4] >> 8 & 0xFF0000) | ($s[5] >> 16 & 0xFF00) | ($s[6] >> 24 & 0xFF);
$s[] = ($s[7] >> 8 & 0xFF0000) | ($s[8] >> 16 & 0xFF00) | ($s[9] >> 24 & 0xFF);
diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md
index a7363d7f59c19..e8146d2a50683 100644
--- a/src/Symfony/Component/Validator/CHANGELOG.md
+++ b/src/Symfony/Component/Validator/CHANGELOG.md
@@ -6,7 +6,55 @@ CHANGELOG
* Add the `filenameCharset` and `filenameCountUnit` options to the `File` constraint
* Deprecate defining custom constraints not supporting named arguments
+
+ Before:
+
+ ```php
+ use Symfony\Component\Validator\Constraint;
+
+ class CustomConstraint extends Constraint
+ {
+ public function __construct(array $options)
+ {
+ // ...
+ }
+ }
+ ```
+
+ After:
+
+ ```php
+ use Symfony\Component\Validator\Attribute\HasNamedArguments;
+ use Symfony\Component\Validator\Constraint;
+
+ class CustomConstraint extends Constraint
+ {
+ #[HasNamedArguments]
+ public function __construct($option1, $option2, $groups, $payload)
+ {
+ // ...
+ }
+ }
+ ```
* Deprecate passing an array of options to the constructors of the constraint classes, pass each option as a dedicated argument instead
+
+ Before:
+
+ ```php
+ new NotNull([
+ 'groups' => ['foo', 'bar'],
+ 'message' => 'a custom constraint violation message',
+ ])
+ ```
+
+ After:
+
+ ```php
+ new NotNull(
+ groups: ['foo', 'bar'],
+ message: 'a custom constraint violation message',
+ )
+ ```
* Add support for ratio checks for SVG files to the `Image` constraint
* Add support for the `otherwise` option in the `When` constraint
* Add support for multiple fields containing nested constraints in `Composite` constraints
diff --git a/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php b/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
index b20ea0df0abe8..20d55f458b6b2 100644
--- a/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
+++ b/src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
/**
@@ -39,6 +40,7 @@ class AtLeastOneOf extends Composite
* @param string|null $messageCollection Failure message for All and Collection inner constraints
* @param bool|null $includeInternalMessages Whether to include inner constraint messages (defaults to true)
*/
+ #[HasNamedArguments]
public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null, ?string $message = null, ?string $messageCollection = null, ?bool $includeInternalMessages = null)
{
if (\is_array($constraints) && !array_is_list($constraints)) {
diff --git a/src/Symfony/Component/Validator/Constraints/Cascade.php b/src/Symfony/Component/Validator/Constraints/Cascade.php
index a3ea71dbf90a9..7d90cfcf7f99f 100644
--- a/src/Symfony/Component/Validator/Constraints/Cascade.php
+++ b/src/Symfony/Component/Validator/Constraints/Cascade.php
@@ -36,6 +36,7 @@ public function __construct(array|string|null $exclude = null, ?array $options =
trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class);
$options = array_merge($exclude, $options ?? []);
+ $options['exclude'] = array_flip((array) ($options['exclude'] ?? []));
} else {
if (\is_array($options)) {
trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class);
diff --git a/src/Symfony/Component/Validator/Constraints/CollectionValidator.php b/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
index a44694345aab0..7d5b20bf16ec6 100644
--- a/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/CollectionValidator.php
@@ -47,7 +47,6 @@ public function validate(mixed $value, Constraint $constraint): void
$context = $this->context;
foreach ($constraint->fields as $field => $fieldConstraint) {
- // bug fix issue #2779
$existsInArray = \is_array($value) && \array_key_exists($field, $value);
$existsInArrayAccess = $value instanceof \ArrayAccess && $value->offsetExists($field);
diff --git a/src/Symfony/Component/Validator/Constraints/Composite.php b/src/Symfony/Component/Validator/Constraints/Composite.php
index deac22cc5570d..ce6283b84f125 100644
--- a/src/Symfony/Component/Validator/Constraints/Composite.php
+++ b/src/Symfony/Component/Validator/Constraints/Composite.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@@ -49,6 +50,7 @@ abstract class Composite extends Constraint
* cached. When constraints are loaded from the cache, no more group
* checks need to be done.
*/
+ #[HasNamedArguments]
public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null)
{
parent::__construct($options, $groups, $payload);
diff --git a/src/Symfony/Component/Validator/Constraints/Compound.php b/src/Symfony/Component/Validator/Constraints/Compound.php
index ac2b5ac9890ca..2618715335b79 100644
--- a/src/Symfony/Component/Validator/Constraints/Compound.php
+++ b/src/Symfony/Component/Validator/Constraints/Compound.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@@ -24,6 +25,7 @@ abstract class Compound extends Composite
/** @var Constraint[] */
public array $constraints = [];
+ #[HasNamedArguments]
public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null)
{
if (isset($options[$this->getCompositeOption()])) {
diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php
index 3c2cc48ba815b..e3e4f47f9e0ae 100644
--- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php
+++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
+
/**
* A sequence of validation groups.
*
@@ -75,6 +77,7 @@ class GroupSequence
*
* @param array $groups The groups in the sequence
*/
+ #[HasNamedArguments]
public function __construct(array $groups)
{
$this->groups = $groups['value'] ?? $groups;
diff --git a/src/Symfony/Component/Validator/Constraints/Image.php b/src/Symfony/Component/Validator/Constraints/Image.php
index 5a4b3e12960e8..d9b7c8822e014 100644
--- a/src/Symfony/Component/Validator/Constraints/Image.php
+++ b/src/Symfony/Component/Validator/Constraints/Image.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
+
/**
* Validates that a file (or a path to a file) is a valid image.
*
@@ -118,6 +120,7 @@ class Image extends File
*
* @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types
*/
+ #[HasNamedArguments]
public function __construct(
?array $options = null,
int|string|null $maxSize = null,
diff --git a/src/Symfony/Component/Validator/Constraints/LocaleValidator.php b/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
index 87d0b26794e71..7f1bfe2651550 100644
--- a/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/LocaleValidator.php
@@ -44,7 +44,7 @@ public function validate(mixed $value, Constraint $constraint): void
$value = \Locale::canonicalize($value);
}
- if (!Locales::exists($value)) {
+ if (null === $value || !Locales::exists($value)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($inputValue))
->setCode(Locale::NO_SUCH_LOCALE_ERROR)
diff --git a/src/Symfony/Component/Validator/Constraints/Sequentially.php b/src/Symfony/Component/Validator/Constraints/Sequentially.php
index 1096a994d0bb4..6389ebb890f3b 100644
--- a/src/Symfony/Component/Validator/Constraints/Sequentially.php
+++ b/src/Symfony/Component/Validator/Constraints/Sequentially.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
/**
@@ -28,6 +29,7 @@ class Sequentially extends Composite
* @param Constraint[]|array|null $constraints An array of validation constraints
* @param string[]|null $groups
*/
+ #[HasNamedArguments]
public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null)
{
if (\is_array($constraints) && !array_is_list($constraints)) {
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
index 2a2e559b95238..d5f48f0ae7ff2 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf
@@ -468,7 +468,7 @@
This value is not a valid Twig template.
- Tato hodnota není platná šablona Twig.
+ Tato hodnota není platná Twig šablona.