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.
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf
index a6be16580c6bd..0acf6dbf23a6c 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf
@@ -468,7 +468,7 @@
This value is not a valid Twig template.
- Este valor não é um modelo Twig válido.
+ Este valor não é um modelo Twig válido.
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php
index ee3798079dc39..fc4d7ce0f3402 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/CascadeTest.php
@@ -27,6 +27,23 @@ public function testCascadeAttribute()
self::assertTrue($loader->loadClassMetadata($metadata));
self::assertSame(CascadingStrategy::CASCADE, $metadata->getCascadingStrategy());
}
+
+ public function testExcludeProperties()
+ {
+ $constraint = new Cascade(['foo', 'bar']);
+
+ self::assertSame(['foo' => 0, 'bar' => 1], $constraint->exclude);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testExcludePropertiesDoctrineStyle()
+ {
+ $constraint = new Cascade(['exclude' => ['foo', 'bar']]);
+
+ self::assertSame(['foo' => 0, 'bar' => 1], $constraint->exclude);
+ }
}
#[Cascade]
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php
index a9429f1883818..5a060e4dab0c4 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/LocaleValidatorTest.php
@@ -89,6 +89,19 @@ public static function getInvalidLocales()
];
}
+ public function testTooLongLocale()
+ {
+ $constraint = new Locale(message: 'myMessage');
+
+ $locale = str_repeat('a', (\defined('INTL_MAX_LOCALE_LEN') ? \INTL_MAX_LOCALE_LEN : 85) + 1);
+ $this->validator->validate($locale, $constraint);
+
+ $this->buildViolation('myMessage')
+ ->setParameter('{{ value }}', '"' . $locale . '"')
+ ->setCode(Locale::NO_SUCH_LOCALE_ERROR)
+ ->assertRaised();
+ }
+
/**
* @dataProvider getUncanonicalizedLocales
*/
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php
index d755df486e140..5abb7487ba328 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php
@@ -63,6 +63,19 @@ public function testValidMac($mac)
$this->assertNoViolation();
}
+ /**
+ * @dataProvider getNotValidMacs
+ */
+ public function testNotValidMac($mac)
+ {
+ $this->validator->validate($mac, new MacAddress());
+
+ $this->buildViolation('This value is not a valid MAC address.')
+ ->setParameter('{{ value }}', '"'.$mac.'"')
+ ->setCode(MacAddress::INVALID_MAC_ERROR)
+ ->assertRaised();
+ }
+
public static function getValidMacs(): array
{
return [
@@ -76,6 +89,17 @@ public static function getValidMacs(): array
];
}
+ public static function getNotValidMacs(): array
+ {
+ return [
+ ['00:00:00:00:00'],
+ ['00:00:00:00:00:0G'],
+ ['GG:GG:GG:GG:GG:GG'],
+ ['GG-GG-GG-GG-GG-GG'],
+ ['GGGG.GGGG.GGGG'],
+ ];
+ }
+
public static function getValidLocalUnicastMacs(): array
{
return [
diff --git a/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php b/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php
index 54a19064e0666..3d6cb7d74cbd4 100644
--- a/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php
+++ b/src/Symfony/Component/VarDumper/Caster/PgSqlCaster.php
@@ -14,7 +14,7 @@
use Symfony\Component\VarDumper\Cloner\Stub;
/**
- * Casts pqsql resources to array representation.
+ * Casts pgsql resources to array representation.
*
* @author Nicolas Grekas
*
@@ -135,9 +135,9 @@ public static function castResult($result, array $a, Stub $stub, bool $isNested)
'name' => pg_field_name($result, $i),
'table' => \sprintf('%s (OID: %s)', pg_field_table($result, $i), pg_field_table($result, $i, true)),
'type' => \sprintf('%s (OID: %s)', pg_field_type($result, $i), pg_field_type_oid($result, $i)),
- 'nullable' => (bool) pg_field_is_null($result, $i),
+ 'nullable' => (bool) (\PHP_VERSION_ID >= 80300 ? pg_field_is_null($result, null, $i) : pg_field_is_null($result, $i)),
'storage' => pg_field_size($result, $i).' bytes',
- 'display' => pg_field_prtlen($result, $i).' chars',
+ 'display' => (\PHP_VERSION_ID >= 80300 ? pg_field_prtlen($result, null, $i) : pg_field_prtlen($result, $i)).' chars',
];
if (' (OID: )' === $field['table']) {
$field['table'] = null;
diff --git a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php
index 5613c5534cd5f..47c2efc69b19f 100644
--- a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php
+++ b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php
@@ -29,7 +29,7 @@ class ResourceCaster
*/
public static function castCurl(\CurlHandle $h, array $a, Stub $stub, bool $isNested): array
{
- trigger_deprecation('symfony/var-dumper', '7.3', 'The "%s()" method is deprecated without replacement.', __METHOD__, CurlCaster::class);
+ trigger_deprecation('symfony/var-dumper', '7.3', 'The "%s()" method is deprecated without replacement.', __METHOD__);
return CurlCaster::castCurl($h, $a, $stub, $isNested);
}
@@ -75,7 +75,7 @@ public static function castStreamContext($stream, array $a, Stub $stub, bool $is
*/
public static function castGd(\GdImage $gd, array $a, Stub $stub, bool $isNested): array
{
- trigger_deprecation('symfony/var-dumper', '7.3', 'The "%s()" method is deprecated without replacement.', __METHOD__, GdCaster::class);
+ trigger_deprecation('symfony/var-dumper', '7.3', 'The "%s()" method is deprecated without replacement.', __METHOD__);
return GdCaster::castGd($gd, $a, $stub, $isNested);
}
@@ -85,7 +85,7 @@ public static function castGd(\GdImage $gd, array $a, Stub $stub, bool $isNested
*/
public static function castOpensslX509(\OpenSSLCertificate $h, array $a, Stub $stub, bool $isNested): array
{
- trigger_deprecation('symfony/var-dumper', '7.3', 'The "%s()" method is deprecated without replacement.', __METHOD__, OpenSSLCaster::class);
+ trigger_deprecation('symfony/var-dumper', '7.3', 'The "%s()" method is deprecated without replacement.', __METHOD__);
return OpenSSLCaster::castOpensslX509($h, $a, $stub, $isNested);
}
diff --git a/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php b/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php
index 42dc901a5f293..0921f62543a5a 100644
--- a/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php
+++ b/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php
@@ -80,12 +80,14 @@ public static function castLazyObjectState($state, array $a, Stub $stub, bool $i
$instance = $a['realInstance'] ?? null;
- $a = ['status' => new ConstStub(match ($a['status']) {
- LazyObjectState::STATUS_INITIALIZED_FULL => 'INITIALIZED_FULL',
- LazyObjectState::STATUS_INITIALIZED_PARTIAL => 'INITIALIZED_PARTIAL',
- LazyObjectState::STATUS_UNINITIALIZED_FULL => 'UNINITIALIZED_FULL',
- LazyObjectState::STATUS_UNINITIALIZED_PARTIAL => 'UNINITIALIZED_PARTIAL',
- }, $a['status'])];
+ if (isset($a['status'])) { // forward-compat with Symfony 8
+ $a = ['status' => new ConstStub(match ($a['status']) {
+ LazyObjectState::STATUS_INITIALIZED_FULL => 'INITIALIZED_FULL',
+ LazyObjectState::STATUS_INITIALIZED_PARTIAL => 'INITIALIZED_PARTIAL',
+ LazyObjectState::STATUS_UNINITIALIZED_FULL => 'UNINITIALIZED_FULL',
+ LazyObjectState::STATUS_UNINITIALIZED_PARTIAL => 'UNINITIALIZED_PARTIAL',
+ }, $a['status'])];
+ }
if ($instance) {
$a['realInstance'] = $instance;
diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php
index 9038d2c04e8a5..b495609133bab 100644
--- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php
+++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php
@@ -183,7 +183,7 @@ abstract class AbstractCloner implements ClonerInterface
':dba' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'],
':dba persistent' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'],
- 'GdImage' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'],
+ 'GdImage' => ['Symfony\Component\VarDumper\Caster\GdCaster', 'castGd'],
'SQLite3Result' => ['Symfony\Component\VarDumper\Caster\SqliteCaster', 'castSqlite3Result'],
@@ -203,8 +203,6 @@ abstract class AbstractCloner implements ClonerInterface
'XmlParser' => ['Symfony\Component\VarDumper\Caster\XmlResourceCaster', 'castXml'],
- 'Socket' => ['Symfony\Component\VarDumper\Caster\SocketCaster', 'castSocket'],
-
'RdKafka' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castRdKafka'],
'RdKafka\Conf' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castConf'],
'RdKafka\KafkaConsumer' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castKafkaConsumer'],
diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ResourceCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ResourceCasterTest.php
index 029f7fb0d6876..946db1dd828a6 100644
--- a/src/Symfony/Component/VarDumper/Tests/Caster/ResourceCasterTest.php
+++ b/src/Symfony/Component/VarDumper/Tests/Caster/ResourceCasterTest.php
@@ -69,14 +69,11 @@ public function testCastDbaPriorToPhp84()
}
/**
- * @requires PHP 8.4
+ * @requires PHP 8.4.2
+ * @requires extension dba
*/
public function testCastDba()
{
- if (\PHP_VERSION_ID < 80402) {
- $this->markTestSkipped('The test cannot be run on PHP 8.4.0 and PHP 8.4.1, see https://github.com/php/php-src/issues/16990');
- }
-
$dba = dba_open(sys_get_temp_dir().'/test.db', 'c');
$this->assertDumpMatchesFormat(
@@ -89,6 +86,7 @@ public function testCastDba()
/**
* @requires PHP 8.4
+ * @requires extension dba
*/
public function testCastDbaOnBuggyPhp84()
{
diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php
index bfa910c745b1c..3a0889a5090b3 100644
--- a/src/Symfony/Component/Yaml/Inline.php
+++ b/src/Symfony/Component/Yaml/Inline.php
@@ -750,7 +750,7 @@ private static function evaluateScalar(string $scalar, int $flags, array &$refer
if (false !== $scalar = $time->getTimestamp()) {
return $scalar;
}
- } catch (\ValueError) {
+ } catch (\DateRangeError|\ValueError) {
// no-op
}