diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 4f17ee596..f1fdd4495 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 4303 | -| Phase A inventory files (M2 ratio SSOT) | 4303 | +| PHP files on vm.php path | 4308 | +| Phase A inventory files (M2 ratio SSOT) | 4308 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 11582 | +| Source constructs flagged (warnings) | 11606 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -247,11 +247,16 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/TokenListReplace.php` | 0 | 1 | | `ext/dom/TokenListSupports.php` | 0 | 1 | | `ext/dom/TokenListToggle.php` | 0 | 1 | -| `ext/dom/VmDom.php` | 0 | 280 | +| `ext/dom/VmDom.php` | 0 | 285 | | `ext/dom/VmDomInstanceInvoke.php` | 0 | 2 | | `ext/dom/VmDomJitDispatch.php` | 0 | 9 | | `ext/dom/VmDomJitFrame.php` | 0 | 1 | | `ext/dom/VmDomTokenList.php` | 0 | 4 | +| `ext/dom/VmDomXPath.php` | 0 | 14 | +| `ext/dom/XPathConstruct.php` | 0 | 2 | +| `ext/dom/XPathEvaluate.php` | 0 | 1 | +| `ext/dom/XPathQuery.php` | 0 | 1 | +| `ext/dom/XPathRegisterNamespace.php` | 0 | 1 | | `ext/filter/BuiltinEnums.php` | 0 | 3 | | `ext/filter/FilterBatchJitHelper.php` | 0 | 6 | | `ext/filter/FilterBooleanJitHelper.php` | 0 | 1 | @@ -5515,14 +5520,12 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/VmDom.php` **Warnings** (review for bootstrap subset): -- new Variable (line 153) -- new Variable (line 154) - new Variable (line 155) - new Variable (line 156) - new Variable (line 157) -- new ClassEntry (line 160) -- new ClassProperty (line 162) -- new ClassProperty (line 163) +- new Variable (line 158) +- new Variable (line 159) +- new ClassEntry (line 162) - new ClassProperty (line 164) - new ClassProperty (line 165) - new ClassProperty (line 166) @@ -5532,268 +5535,275 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new ClassProperty (line 170) - new ClassProperty (line 171) - new ClassProperty (line 172) +- new ClassProperty (line 173) - new ClassProperty (line 174) -- new NodeCloneNode (line 176) -- new NodeReplaceChild (line 178) -- new NodeInsertBefore (line 180) -- new NodeRemoveChild (line 182) -- new NodeIsSameNode (line 184) -- new NodeIsEqualNode (line 187) -- new NodeHasChildNodes (line 190) -- new NodeContains (line 193) -- new NodeGetRootNode (line 197) -- new NodeAppend (line 200) -- new NodeReplaceChildren (line 203) -- new NodePrepend (line 206) -- new NodeBefore (line 208) -- new NodeAfter (line 210) -- new NodeReplaceWith (line 212) -- new NodeRemove (line 214) -- new NodeLookupPrefix (line 216) -- new NodeLookupNamespaceURI (line 218) -- new NodeGetLineNo (line 220) -- new NodeGetNodePath (line 222) -- new NodeHasAttributes (line 224) -- new NodeIsDefaultNamespace (line 226) -- new NodeIsSupported (line 228) -- new NodeCompareDocumentPosition (line 231) -- new NodeNormalize (line 242) -- new NodeC14N (line 244) -- new NodeC14NFile (line 247) -- new ClassEntry (line 252) -- new ClassProperty (line 255) -- new TextSplitText (line 256) -- new ClassEntry (line 261) -- new ClassProperty (line 264) -- new CharacterDataAppendData (line 265) -- new CharacterDataDeleteData (line 268) -- new CharacterDataInsertData (line 271) -- new CharacterDataReplaceData (line 274) -- new CharacterDataSubstringData (line 277) -- new ClassEntry (line 282) -- new ClassProperty (line 285) -- new ClassEntry (line 288) -- new ClassProperty (line 291) -- new ClassEntry (line 294) -- new ClassProperty (line 297) -- new ClassProperty (line 298) +- new ClassProperty (line 176) +- new NodeCloneNode (line 178) +- new NodeReplaceChild (line 180) +- new NodeInsertBefore (line 182) +- new NodeRemoveChild (line 184) +- new NodeIsSameNode (line 186) +- new NodeIsEqualNode (line 189) +- new NodeHasChildNodes (line 192) +- new NodeContains (line 195) +- new NodeGetRootNode (line 199) +- new NodeAppend (line 202) +- new NodeReplaceChildren (line 205) +- new NodePrepend (line 208) +- new NodeBefore (line 210) +- new NodeAfter (line 212) +- new NodeReplaceWith (line 214) +- new NodeRemove (line 216) +- new NodeLookupPrefix (line 218) +- new NodeLookupNamespaceURI (line 220) +- new NodeGetLineNo (line 222) +- new NodeGetNodePath (line 224) +- new NodeHasAttributes (line 226) +- new NodeIsDefaultNamespace (line 228) +- new NodeIsSupported (line 230) +- new NodeCompareDocumentPosition (line 233) +- new NodeNormalize (line 244) +- new NodeC14N (line 246) +- new NodeC14NFile (line 249) +- new ClassEntry (line 254) +- new ClassProperty (line 257) +- new TextSplitText (line 258) +- new ClassEntry (line 263) +- new ClassProperty (line 266) +- new CharacterDataAppendData (line 267) +- new CharacterDataDeleteData (line 270) +- new CharacterDataInsertData (line 273) +- new CharacterDataReplaceData (line 276) +- new CharacterDataSubstringData (line 279) +- new ClassEntry (line 284) +- new ClassProperty (line 287) +- new ClassEntry (line 290) +- new ClassProperty (line 293) +- new ClassEntry (line 296) - new ClassProperty (line 299) - new ClassProperty (line 300) -- new ClassEntry (line 303) -- new ClassProperty (line 312) -- new NodeListItem (line 313) -- new NodeListCount (line 315) -- new NodeListRewind (line 317) -- new NodeListValid (line 319) -- new NodeListCurrent (line 321) -- new NodeListKey (line 323) -- new NodeListNext (line 325) -- new ClassEntry (line 329) -- new ClassProperty (line 338) -- new NamedNodeMapItem (line 339) -- new NamedNodeMapGetNamedItem (line 341) -- new NamedNodeMapGetNamedItemNS (line 344) -- new NamedNodeMapCount (line 347) -- new NamedNodeMapRewind (line 349) -- new NamedNodeMapValid (line 351) -- new NamedNodeMapCurrent (line 353) -- new NamedNodeMapKey (line 355) -- new NamedNodeMapNext (line 357) -- new ClassEntry (line 362) -- new ClassProperty (line 365) -- new ClassProperty (line 366) -- new TokenListAdd (line 367) -- new TokenListRemove (line 369) -- new TokenListContains (line 371) -- new TokenListToggle (line 373) -- new TokenListItem (line 375) -- new TokenListReplace (line 377) -- new TokenListSupports (line 379) -- new TokenListCount (line 381) -- new ClassEntry (line 386) -- new ImplementationCreateDocument (line 388) -- new ImplementationCreateDocumentType (line 390) -- new ImplementationGetFeature (line 392) -- new ImplementationHasFeature (line 394) -- new ClassEntry (line 399) -- new ClassProperty (line 402) -- new ClassProperty (line 403) -- new ClassProperty (line 404) -- new ClassProperty (line 405) -- new ClassEntry (line 408) -- new ClassProperty (line 411) -- new ClassProperty (line 412) -- new ClassProperty (line 413) -- new ClassProperty (line 414) -- new ClassEntry (line 417) -- new DocumentConstruct (line 420) -- new ClassProperty (line 424) -- new ClassProperty (line 425) -- new ClassProperty (line 426) -- new ClassProperty (line 427) +- new ClassProperty (line 301) +- new ClassProperty (line 302) +- new ClassEntry (line 305) +- new ClassProperty (line 314) +- new NodeListItem (line 315) +- new NodeListCount (line 317) +- new NodeListRewind (line 319) +- new NodeListValid (line 321) +- new NodeListCurrent (line 323) +- new NodeListKey (line 325) +- new NodeListNext (line 327) +- new ClassEntry (line 331) +- new ClassProperty (line 340) +- new NamedNodeMapItem (line 341) +- new NamedNodeMapGetNamedItem (line 343) +- new NamedNodeMapGetNamedItemNS (line 346) +- new NamedNodeMapCount (line 349) +- new NamedNodeMapRewind (line 351) +- new NamedNodeMapValid (line 353) +- new NamedNodeMapCurrent (line 355) +- new NamedNodeMapKey (line 357) +- new NamedNodeMapNext (line 359) +- new ClassEntry (line 364) +- new ClassProperty (line 367) +- new ClassProperty (line 368) +- new TokenListAdd (line 369) +- new TokenListRemove (line 371) +- new TokenListContains (line 373) +- new TokenListToggle (line 375) +- new TokenListItem (line 377) +- new TokenListReplace (line 379) +- new TokenListSupports (line 381) +- new TokenListCount (line 383) +- new ClassEntry (line 388) +- new XPathConstruct (line 390) +- new XPathQuery (line 394) +- new XPathEvaluate (line 396) +- new XPathRegisterNamespace (line 398) +- new ClassEntry (line 403) +- new ImplementationCreateDocument (line 405) +- new ImplementationCreateDocumentType (line 407) +- new ImplementationGetFeature (line 409) +- new ImplementationHasFeature (line 411) +- new ClassEntry (line 416) +- new ClassProperty (line 419) +- new ClassProperty (line 420) +- new ClassProperty (line 421) +- new ClassProperty (line 422) +- new ClassEntry (line 425) - new ClassProperty (line 428) - new ClassProperty (line 429) - new ClassProperty (line 430) - new ClassProperty (line 431) -- new ClassProperty (line 432) -- new ClassProperty (line 433) -- new ClassProperty (line 434) -- new DocumentLoadXML (line 435) -- new DocumentLoad (line 437) -- new DocumentLoadHTML (line 439) -- new DocumentCreateElement (line 442) -- new DocumentCreateElementNS (line 444) -- new DocumentCreateAttributeNS (line 446) -- new DocumentCreateAttribute (line 448) -- new DocumentCreateDocumentFragment (line 450) -- new DocumentCreateEntityReference (line 452) -- new DocumentCreateTextNode (line 455) -- new DocumentCreateComment (line 458) -- new DocumentAppendChild (line 461) -- new DocumentSaveXML (line 463) -- new DocumentSaveHTML (line 465) -- new DocumentSaveHTMLFile (line 468) -- new DocumentGetElementsByTagName (line 471) -- new DocumentGetElementsByTagNameNS (line 473) -- new DocumentGetElementById (line 476) -- new DocumentImportNode (line 478) -- new DocumentAdoptNode (line 480) -- new DocumentRegisterNodeClass (line 483) -- new DocumentNormalizeDocument (line 486) -- new DocumentXInclude (line 489) -- new DocumentSchemaValidate (line 491) -- new DocumentRelaxNGValidate (line 494) -- new ClassEntry (line 499) -- new ClassProperty (line 502) -- new ClassProperty (line 503) -- new ElementAppendChild (line 504) -- new ElementGetAttribute (line 506) -- new ElementGetAttributeNode (line 508) -- new ElementGetAttributeNS (line 510) -- new ElementHasAttribute (line 512) -- new ElementHasAttributeNS (line 514) -- new ElementRemoveAttribute (line 516) -- new ElementRemoveAttributeNode (line 518) -- new ElementSetAttribute (line 520) -- new ElementSetAttributeNode (line 522) -- new ElementSetAttributeNS (line 524) -- new ElementRemoveAttributeNS (line 526) -- new ElementSetIdAttribute (line 528) -- new ElementSetIdAttributeNS (line 531) -- new ElementGetElementsByTagName (line 534) -- new ElementGetElementsByTagNameNS (line 536) -- new ElementGetAttributeNames (line 540) -- new ElementInsertAdjacentHTML (line 545) -- new ElementInsertAdjacentElement (line 550) -- new ElementInsertAdjacentText (line 555) -- new ElementToggleAttribute (line 560) -- new ElementGetInnerHTML (line 565) -- new ElementGetOuterHTML (line 568) -- new ClassProperty (line 573) -- new ClassEntry (line 577) -- new ClassProperty (line 580) -- new FragmentAppendChild (line 581) -- new FragmentAppendXML (line 583) -- new ObjectEntry (line 597) -- new DomNodeState (line 599) -- new Variable (line 607) -- new ObjectEntry (line 630) -- new DomNodeState (line 635) -- new Variable (line 660) -- new ObjectEntry (line 688) -- new DomNodeState (line 709) -- new DomNodeState (line 759) -- new ObjectEntry (line 776) -- new DomNodeState (line 781) -- new Variable (line 797) -- new ObjectEntry (line 813) -- new DomNodeState (line 818) -- new Variable (line 835) -- new ObjectEntry (line 850) -- new DomNodeState (line 858) -- new Variable (line 870) -- new DOMException (line 887) -- new Variable (line 891) -- new Variable (line 897) -- new DOMException (line 906) -- new Variable (line 935) -- new DOMException (line 948) -- new DOMException (line 961) -- new Variable (line 973) -- new DOMException (line 1039) -- new DOMException (line 1091) -- new DOMException (line 1104) -- new DOMException (line 1127) -- new DOMException (line 1141) -- new DOMException (line 1167) -- new DOMException (line 1222) -- new DOMException (line 1249) -- new DOMException (line 1253) +- new ClassEntry (line 434) +- new DocumentConstruct (line 437) +- new ClassProperty (line 441) +- new ClassProperty (line 442) +- new ClassProperty (line 443) +- new ClassProperty (line 444) +- new ClassProperty (line 445) +- new ClassProperty (line 446) +- new ClassProperty (line 447) +- new ClassProperty (line 448) +- new ClassProperty (line 449) +- new ClassProperty (line 450) +- new ClassProperty (line 451) +- new DocumentLoadXML (line 452) +- new DocumentLoad (line 454) +- new DocumentLoadHTML (line 456) +- new DocumentCreateElement (line 459) +- new DocumentCreateElementNS (line 461) +- new DocumentCreateAttributeNS (line 463) +- new DocumentCreateAttribute (line 465) +- new DocumentCreateDocumentFragment (line 467) +- new DocumentCreateEntityReference (line 469) +- new DocumentCreateTextNode (line 472) +- new DocumentCreateComment (line 475) +- new DocumentAppendChild (line 478) +- new DocumentSaveXML (line 480) +- new DocumentSaveHTML (line 482) +- new DocumentSaveHTMLFile (line 485) +- new DocumentGetElementsByTagName (line 488) +- new DocumentGetElementsByTagNameNS (line 490) +- new DocumentGetElementById (line 493) +- new DocumentImportNode (line 495) +- new DocumentAdoptNode (line 497) +- new DocumentRegisterNodeClass (line 500) +- new DocumentNormalizeDocument (line 503) +- new DocumentXInclude (line 506) +- new DocumentSchemaValidate (line 508) +- new DocumentRelaxNGValidate (line 511) +- new ClassEntry (line 516) +- new ClassProperty (line 519) +- new ClassProperty (line 520) +- new ElementAppendChild (line 521) +- new ElementGetAttribute (line 523) +- new ElementGetAttributeNode (line 525) +- new ElementGetAttributeNS (line 527) +- new ElementHasAttribute (line 529) +- new ElementHasAttributeNS (line 531) +- new ElementRemoveAttribute (line 533) +- new ElementRemoveAttributeNode (line 535) +- new ElementSetAttribute (line 537) +- new ElementSetAttributeNode (line 539) +- new ElementSetAttributeNS (line 541) +- new ElementRemoveAttributeNS (line 543) +- new ElementSetIdAttribute (line 545) +- new ElementSetIdAttributeNS (line 548) +- new ElementGetElementsByTagName (line 551) +- new ElementGetElementsByTagNameNS (line 553) +- new ElementGetAttributeNames (line 557) +- new ElementInsertAdjacentHTML (line 562) +- new ElementInsertAdjacentElement (line 567) +- new ElementInsertAdjacentText (line 572) +- new ElementToggleAttribute (line 577) +- new ElementGetInnerHTML (line 582) +- new ElementGetOuterHTML (line 585) +- new ClassProperty (line 590) +- new ClassEntry (line 594) +- new ClassProperty (line 597) +- new FragmentAppendChild (line 598) +- new FragmentAppendXML (line 600) +- new ObjectEntry (line 614) +- new DomNodeState (line 616) +- new Variable (line 624) +- new ObjectEntry (line 647) +- new DomNodeState (line 652) +- new Variable (line 677) +- new ObjectEntry (line 705) +- new DomNodeState (line 726) +- new DomNodeState (line 776) +- new ObjectEntry (line 793) +- new DomNodeState (line 798) +- new Variable (line 814) +- new ObjectEntry (line 830) +- new DomNodeState (line 835) +- new Variable (line 852) +- new ObjectEntry (line 867) +- new DomNodeState (line 875) +- new Variable (line 887) +- new DOMException (line 904) +- new Variable (line 908) +- new Variable (line 914) +- new DOMException (line 923) +- new Variable (line 952) +- new DOMException (line 965) +- new DOMException (line 978) +- new Variable (line 990) +- new DOMException (line 1056) +- new DOMException (line 1108) +- new DOMException (line 1121) +- new DOMException (line 1144) +- new DOMException (line 1158) +- new DOMException (line 1184) +- new DOMException (line 1239) - new DOMException (line 1266) - new DOMException (line 1270) -- new DOMException (line 1280) -- new ObjectEntry (line 1651) -- new DomNodeState (line 1656) -- new ObjectEntry (line 1672) -- new DomNodeState (line 1677) -- new DOMException (line 1716) -- new DOMException (line 1726) -- new DOMException (line 1739) -- new DOMException (line 1758) -- new DOMException (line 1772) -- new ObjectEntry (line 1798) -- new DomNodeState (line 1803) -- new Variable (line 1812) -- new ObjectEntry (line 1822) -- new DomNodeState (line 1827) -- new Variable (line 1832) -- new ObjectEntry (line 2306) -- new DomNodeState (line 2308) -- new DOMException (line 2455) -- new DOMException (line 2481) -- new Variable (line 2495) -- new DOMException (line 2509) -- new DOMException (line 2512) -- new DOMException (line 2520) -- new DOMException (line 2546) -- new DOMException (line 2559) -- new DOMException (line 2745) -- new DOMException (line 2942) -- new DOMException (line 2946) -- new DOMException (line 3035) -- new DOMException (line 3039) -- new DOMException (line 3097) +- new DOMException (line 1283) +- new DOMException (line 1287) +- new DOMException (line 1297) +- new ObjectEntry (line 1668) +- new DomNodeState (line 1673) +- new ObjectEntry (line 1689) +- new DomNodeState (line 1694) +- new DOMException (line 1733) +- new DOMException (line 1743) +- new DOMException (line 1756) +- new DOMException (line 1775) +- new DOMException (line 1789) +- new ObjectEntry (line 1815) +- new DomNodeState (line 1820) +- new Variable (line 1829) +- new ObjectEntry (line 1839) +- new DomNodeState (line 1844) +- new Variable (line 1849) +- new ObjectEntry (line 2323) +- new DomNodeState (line 2325) +- new DOMException (line 2472) +- new DOMException (line 2498) +- new Variable (line 2512) +- new DOMException (line 2526) +- new DOMException (line 2529) +- new DOMException (line 2537) +- new DOMException (line 2563) +- new DOMException (line 2576) +- new DOMException (line 2762) +- new DOMException (line 2959) +- new DOMException (line 2963) +- new DOMException (line 3052) +- new DOMException (line 3056) - new DOMException (line 3114) -- new DOMException (line 3143) -- new DOMException (line 3184) -- new DOMException (line 3209) -- new DOMException (line 3683) -- new Variable (line 3707) -- new DOMException (line 3887) -- new DOMException (line 3968) -- new DOMException (line 4010) -- new ObjectEntry (line 4095) -- new DomNodeState (line 4099) -- new Variable (line 4106) -- new ObjectEntry (line 4306) -- new DomNodeState (line 4310) -- new Variable (line 4317) -- new ObjectEntry (line 4337) -- new DomNodeState (line 4340) -- new Variable (line 4348) -- new DOMException (line 4543) -- new DOMException (line 4550) -- new DOMException (line 4557) -- new DOMException (line 4561) -- new DOMException (line 4564) -- new DOMException (line 4573) -- new Variable (line 5266) -- new Variable (line 5279) -- new DOMException (line 5357) -- new DOMException (line 5368) -- new DOMException (line 5434) -- new DOMException (line 5442) -- 268 class method(s) +- new DOMException (line 3131) +- new DOMException (line 3160) +- new DOMException (line 3201) +- new DOMException (line 3226) +- new DOMException (line 3700) +- new Variable (line 3724) +- new DOMException (line 3904) +- new DOMException (line 3985) +- new DOMException (line 4027) +- new ObjectEntry (line 4112) +- new DomNodeState (line 4116) +- new Variable (line 4123) +- new ObjectEntry (line 4323) +- new DomNodeState (line 4327) +- new Variable (line 4334) +- new ObjectEntry (line 4354) +- new DomNodeState (line 4357) +- new Variable (line 4365) +- new DOMException (line 4567) +- new DOMException (line 4574) +- new DOMException (line 4581) +- new DOMException (line 4585) +- new DOMException (line 4588) +- new DOMException (line 4597) +- new Variable (line 5290) +- new Variable (line 5303) +- new DOMException (line 5381) +- new DOMException (line 5392) +- new DOMException (line 5458) +- new DOMException (line 5466) +- 269 class method(s) - 2 closure(s) ### `ext/dom/VmDomInstanceInvoke.php` @@ -5828,6 +5838,45 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - 17 class method(s) - 1 closure(s) +### `ext/dom/VmDomXPath.php` + +**Warnings** (review for bootstrap subset): +- new ObjectEntry (line 22) +- new DomNodeState (line 25) +- new Variable (line 63) +- new Variable (line 69) +- new Variable (line 75) +- new DOMException (line 96) +- new DOMException (line 102) +- new DOMException (line 145) +- new DOMException (line 265) +- new DOMException (line 349) +- new DOMException (line 369) +- new DOMException (line 378) +- 21 class method(s) +- 1 closure(s) + +### `ext/dom/XPathConstruct.php` + +**Warnings** (review for bootstrap subset): +- new DomNodeState (line 45) +- 1 class method(s) + +### `ext/dom/XPathEvaluate.php` + +**Warnings** (review for bootstrap subset): +- 4 class method(s) + +### `ext/dom/XPathQuery.php` + +**Warnings** (review for bootstrap subset): +- 3 class method(s) + +### `ext/dom/XPathRegisterNamespace.php` + +**Warnings** (review for bootstrap subset): +- 2 class method(s) + ### `ext/filter/BuiltinEnums.php` **Warnings** (review for bootstrap subset): diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json index 44e1353b3..2b7304c45 100644 --- a/docs/bootstrap-profile.json +++ b/docs/bootstrap-profile.json @@ -258,6 +258,11 @@ "ext/dom/VmDomJitDispatch.php", "ext/dom/VmDomJitFrame.php", "ext/dom/VmDomTokenList.php", + "ext/dom/VmDomXPath.php", + "ext/dom/XPathConstruct.php", + "ext/dom/XPathEvaluate.php", + "ext/dom/XPathQuery.php", + "ext/dom/XPathRegisterNamespace.php", "ext/filter/BuiltinEnums.php", "ext/filter/FilterBatchJitHelper.php", "ext/filter/FilterBooleanJitHelper.php", @@ -4537,9 +4542,9 @@ "test/bootstrap-aot/lib_opcode/main.php" ], "totals": { - "inventory_files": 4303, + "inventory_files": 4308, "excluded": 0, - "eligible": 4303, + "eligible": 4308, "aot_lint_targets": 113, "aot_link_targets": 100, "aot_link_lib_targets": 1 diff --git a/ext/dom/DomConstants.php b/ext/dom/DomConstants.php index 85b39a4d6..a071ae869 100644 --- a/ext/dom/DomConstants.php +++ b/ext/dom/DomConstants.php @@ -34,6 +34,9 @@ final class DomConstants /** Internal marker for {@see VmDom::createTokenList()} handles (#16876). */ public const XML_TOKENLIST = -3; + /** Internal marker for {@see VmDom::createXPath()} handles (#6066). */ + public const XML_XPATH = -4; + /** DOMNode::compareDocumentPosition() flags (php-src ext/dom/node.c; #14448). */ public const DOCUMENT_POSITION_DISCONNECTED = 0x01; diff --git a/ext/dom/DomNodeState.php b/ext/dom/DomNodeState.php index b1049a4c7..e73fbc467 100644 --- a/ext/dom/DomNodeState.php +++ b/ext/dom/DomNodeState.php @@ -90,6 +90,16 @@ final class DomNodeState /** Owning element object id for {@see DomConstants::XML_TOKENLIST} handles (#16876). */ public ?int $tokenListElementId = null; + /** Owning document object id for {@see DomConstants::XML_XPATH} handles (#6066). */ + public ?int $xpathDocumentId = null; + + /** + * Registered namespace prefixes for {@see DomConstants::XML_XPATH} (#6066). + * + * @var array + */ + public array $xpathNamespaces = []; + /** * Ordered unique tokens for {@see DomConstants::XML_TOKENLIST} (#16876). * diff --git a/ext/dom/VmDom.php b/ext/dom/VmDom.php index 1f959e7e5..7dde96906 100644 --- a/ext/dom/VmDom.php +++ b/ext/dom/VmDom.php @@ -62,6 +62,8 @@ final class VmDom public const CLASS_TOKEN_LIST = 'domtokenlist'; + public const CLASS_XPATH = 'domxpath'; + public const PROP_FORMAT_OUTPUT = 'formatOutput'; public const PROP_IMPLEMENTATION = 'implementation'; @@ -383,6 +385,21 @@ public static function registerClasses(Context $ctx): void $ctx->classes[self::CLASS_TOKEN_LIST] = $tokenList; } + $xpath = new ClassEntry('DOMXPath'); + $xpath->isInternal = true; + $xpathConstruct = new XPathConstruct(); + $xpath->constructor = $xpathConstruct; + $xpath->methods['__construct'] = $xpathConstruct; + $xpath->methodVisibility['__construct'] = $pub; + $xpath->methods['query'] = new XPathQuery(); + $xpath->methodVisibility['query'] = $pub; + $xpath->methods['evaluate'] = new XPathEvaluate(); + $xpath->methodVisibility['evaluate'] = $pub; + $xpath->methods['registernamespace'] = new XPathRegisterNamespace(); + $xpath->methodVisibility['registernamespace'] = $pub; + $xpath->methodNames['registernamespace'] = 'registerNamespace'; + $ctx->classes[self::CLASS_XPATH] = $xpath; + $impl = new ClassEntry('DOMImplementation'); $impl->isInternal = true; $impl->methods['createdocument'] = new ImplementationCreateDocument(); @@ -4358,6 +4375,13 @@ public static function isTokenList(ObjectEntry $entry): bool && DomConstants::XML_TOKENLIST === DomRegistry::state($entry)->nodeType; } + public static function isXPath(ObjectEntry $entry): bool + { + return self::CLASS_XPATH === strtolower($entry->class->name) + && DomRegistry::has($entry) + && DomConstants::XML_XPATH === DomRegistry::state($entry)->nodeType; + } + public static function syncElementClassList(Context $ctx, ObjectEntry $element): void { if (!CompilerVersion::supportsDomTokenList() || !self::isElement($element)) { diff --git a/ext/dom/VmDomXPath.php b/ext/dom/VmDomXPath.php new file mode 100644 index 000000000..6d480590b --- /dev/null +++ b/ext/dom/VmDomXPath.php @@ -0,0 +1,431 @@ +classes[VmDom::CLASS_XPATH] ?? null; + if (null === $class) { + throw new \LogicException('DOMXPath is not registered in this compiler build'); + } + + $entry = new ObjectEntry($class); + $entry->constructed = true; + + $state = new DomNodeState(); + $state->nodeType = DomConstants::XML_XPATH; + $state->nodeName = 'DOMXPath'; + $state->xpathDocumentId = $document->id; + DomRegistry::attach($entry, $state); + + return $entry; + } + + public static function registerNamespace(ObjectEntry $xpath, string $prefix, string $namespaceUri): bool + { + self::ensureXPath($xpath); + DomRegistry::state($xpath)->xpathNamespaces[$prefix] = $namespaceUri; + + return true; + } + + public static function query( + Context $ctx, + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode = null, + bool $registerNodeNS = false + ): Variable { + $nodeIds = self::evaluateNodeSet($xpath, $expression, $contextNode, $registerNodeNS); + + return VmDom::createNodeList($ctx, $nodeIds); + } + + public static function evaluate( + Context $ctx, + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode = null, + bool $registerNodeNS = false + ): Variable { + $expression = trim($expression); + if (self::isBooleanExpression($expression)) { + $var = new Variable(Variable::TYPE_BOOL); + $var->bool(self::evaluateBoolean($xpath, $expression, $contextNode)); + + return $var; + } + if (self::isNumericExpression($expression)) { + $var = new Variable(Variable::TYPE_FLOAT); + $var->float(self::evaluateNumber($xpath, $expression, $contextNode)); + + return $var; + } + if (self::isStringExpression($expression)) { + $var = new Variable(Variable::TYPE_STRING); + $var->string(self::evaluateString($xpath, $expression, $contextNode)); + + return $var; + } + + return self::query($ctx, $xpath, $expression, $contextNode, $registerNodeNS); + } + + /** + * @return list + */ + private static function evaluateNodeSet( + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode, + bool $registerNodeNS + ): array { + self::ensureXPath($xpath); + $expression = trim($expression); + if ('' === $expression) { + throw new \DOMException('Invalid expression'); + } + + $state = DomRegistry::state($xpath); + $document = DomRegistry::entry($state->xpathDocumentId ?? 0); + if (null === $document || !VmDom::isDocument($document)) { + throw new \DOMException('Couldn\'t allocate a DOMXPath object'); + } + + $context = $contextNode ?? $document; + if (!VmDom::isDomNode($context)) { + throw new \TypeError('DOMXPath::query(): Argument #2 ($context) must be of type ?DOMNode, DOMNode given'); + } + if ($registerNodeNS) { + self::registerContextNodeNamespaces($xpath, $context); + } + + if (preg_match( + '~^//([*\w][\w:-]*)(?:\[@([^\]=]+)=["\']([^"\']*)["\']\])?$~', + $expression, + $matches + )) { + $tag = $matches[1]; + $attr = $matches[2] ?? null; + $attrValue = $matches[3] ?? null; + $nodeIds = self::collectDescendantElements($context, $tag, $state->xpathNamespaces); + if (null !== $attr) { + $nodeIds = array_values(array_filter( + $nodeIds, + static fn (int $id): bool => self::elementAttributeEquals( + DomRegistry::entry($id), + $attr, + $attrValue, + $state->xpathNamespaces + ) + )); + } + + return $nodeIds; + } + + if (preg_match('~^/(.+)$~', $expression, $matches)) { + return self::evaluateAbsolutePath($context, $matches[1], $state->xpathNamespaces); + } + + if (preg_match('~^([*\w][\w:-]*)$~', $expression, $matches)) { + return self::collectChildElements($context, $matches[1], $state->xpathNamespaces); + } + + throw new \DOMException('Invalid expression'); + } + + private static function ensureXPath(ObjectEntry $xpath): void + { + if (!DomRegistry::has($xpath) || DomConstants::XML_XPATH !== DomRegistry::state($xpath)->nodeType) { + throw new \LogicException('DOMXPath method called on non-xpath object'); + } + } + + private static function registerContextNodeNamespaces(ObjectEntry $xpath, ObjectEntry $context): void + { + if (!VmDom::isElement($context)) { + return; + } + $state = DomRegistry::state($context); + foreach ($state->namespaceDeclarations as $prefix => $uri) { + self::registerNamespace($xpath, $prefix, $uri); + } + } + + /** + * @param array $namespaces + * + * @return list + */ + private static function collectDescendantElements( + ObjectEntry $context, + string $tag, + array $namespaces + ): array { + [$localName, $namespaceUri] = self::resolveQName($tag, $namespaces); + if (null !== $namespaceUri) { + return VmDom::collectElementsByTagNameNS($context, $namespaceUri, $localName); + } + + return VmDom::collectElementsByTagName($context, $localName); + } + + /** + * @param array $namespaces + * + * @return list + */ + private static function collectChildElements( + ObjectEntry $context, + string $tag, + array $namespaces + ): array { + [$localName, $namespaceUri] = self::resolveQName($tag, $namespaces); + $matches = []; + if (!DomRegistry::has($context)) { + return $matches; + } + foreach (DomRegistry::state($context)->childIds as $childId) { + $child = DomRegistry::entry($childId); + if (null === $child || !VmDom::isElement($child)) { + continue; + } + if (self::elementMatchesTag($child, $localName, $namespaceUri)) { + $matches[] = $child->id; + } + } + + return $matches; + } + + /** + * @param array $namespaces + * + * @return list + */ + private static function evaluateAbsolutePath( + ObjectEntry $context, + string $path, + array $namespaces + ): array { + $segments = explode('/', $path); + $current = VmDom::isDocument($context) + ? $context->getProperty(VmDom::PROP_DOCUMENT_ELEMENT)->resolveIndirect()->toObject() + : $context; + $lastIndex = \count($segments) - 1; + foreach ($segments as $index => $segment) { + if ('' === $segment) { + continue; + } + if (!VmDom::isElement($current)) { + return []; + } + $children = self::collectChildElements($current, $segment, $namespaces); + if ([] === $children) { + return []; + } + if ($index === $lastIndex) { + return $children; + } + $current = DomRegistry::entry($children[0]); + if (null === $current) { + return []; + } + } + + return []; + } + + /** + * @param array $namespaces + * + * @return array{0: string, 1: ?string} + */ + private static function resolveQName(string $qName, array $namespaces): array + { + if ('*' === $qName) { + return ['*', null]; + } + if (!str_contains($qName, ':')) { + return [$qName, null]; + } + [$prefix, $local] = explode(':', $qName, 2); + if (!isset($namespaces[$prefix])) { + throw new \DOMException('Undefined namespace prefix'); + } + + return [$local, $namespaces[$prefix]]; + } + + private static function elementMatchesTag( + ObjectEntry $element, + string $localName, + ?string $namespaceUri + ): bool { + if (!VmDom::isElement($element)) { + return false; + } + $name = VmDom::readLocalName($element); + $nameMatch = '*' === $localName || $name === $localName; + if (!$nameMatch) { + return false; + } + if (null === $namespaceUri) { + return true; + } + $ns = VmDom::readNamespaceUri($element) ?? ''; + + return '*' === $namespaceUri || $ns === $namespaceUri; + } + + /** + * @param array $namespaces + */ + private static function elementAttributeEquals( + ?ObjectEntry $element, + string $attrName, + string $attrValue, + array $namespaces + ): bool { + if (null === $element || !VmDom::isElement($element)) { + return false; + } + if (str_contains($attrName, ':')) { + [$prefix, $local] = explode(':', $attrName, 2); + $namespace = $namespaces[$prefix] ?? null; + if (null === $namespace) { + return false; + } + + return VmDom::getAttributeNS($element, $namespace, $local) === $attrValue; + } + + return VmDom::getAttributeNS($element, null, $attrName) === $attrValue; + } + + private static function isBooleanExpression(string $expression): bool + { + return (bool) preg_match('~^(true|false|boolean\()~i', $expression); + } + + private static function isNumericExpression(string $expression): bool + { + return (bool) preg_match('~^(number|count)\(~i', $expression); + } + + private static function isStringExpression(string $expression): bool + { + return (bool) preg_match('~^string\(~i', $expression); + } + + private static function evaluateBoolean( + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode + ): bool { + if (0 === strcasecmp($expression, 'true()')) { + return true; + } + if (0 === strcasecmp($expression, 'false()')) { + return false; + } + if (preg_match('~^boolean\((.+)\)$~i', $expression, $matches)) { + $value = self::evaluateScalar($xpath, trim($matches[1]), $contextNode); + + return self::booleanize($value); + } + + throw new \DOMException('Invalid expression'); + } + + private static function evaluateNumber( + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode + ): float { + if (preg_match('~^count\((.+)\)$~i', $expression, $matches)) { + return (float) \count(self::evaluateNodeSet($xpath, trim($matches[1]), $contextNode, false)); + } + if (preg_match('~^number\((.+)\)$~i', $expression, $matches)) { + $value = self::evaluateScalar($xpath, trim($matches[1]), $contextNode); + if (is_numeric($value)) { + return (float) $value; + } + + return NAN; + } + + throw new \DOMException('Invalid expression'); + } + + private static function evaluateString( + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode + ): string { + if (!preg_match('~^string\((.+)\)$~i', $expression, $matches)) { + throw new \DOMException('Invalid expression'); + } + $value = self::evaluateScalar($xpath, trim($matches[1]), $contextNode); + if (is_string($value)) { + return $value; + } + if (is_float($value) || is_int($value)) { + return (string) $value; + } + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + return ''; + } + + private static function evaluateScalar( + ObjectEntry $xpath, + string $expression, + ?ObjectEntry $contextNode + ): mixed { + $nodeIds = self::evaluateNodeSet($xpath, $expression, $contextNode, false); + if ([] === $nodeIds) { + return ''; + } + $node = DomRegistry::entry($nodeIds[0]); + if (null === $node) { + return ''; + } + if (VmDom::isElement($node) || VmDom::isTextNode($node)) { + return VmDom::readNodeValue($node) ?? ''; + } + + return ''; + } + + private static function booleanize(mixed $value): bool + { + if (is_bool($value)) { + return $value; + } + if (is_numeric($value)) { + return 0.0 !== (float) $value && 0 !== $value; + } + if (is_string($value)) { + return '' !== $value; + } + if (is_array($value)) { + return [] !== $value; + } + + return false; + } +} diff --git a/ext/dom/XPathConstruct.php b/ext/dom/XPathConstruct.php new file mode 100644 index 000000000..9bec4b740 --- /dev/null +++ b/ext/dom/XPathConstruct.php @@ -0,0 +1,51 @@ +receiver($frame, VmDom::CLASS_XPATH, 'DOMXPath::__construct()'); + if (\count($frame->calledArgs) < 2) { + throw new \LogicException('DOMXPath::__construct() expects at least 1 argument'); + } + $documentVar = $frame->calledArgs[1]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $documentVar->type) { + throw new \TypeError(sprintf( + 'DOMXPath::__construct(): Argument #1 ($document) must be of type DOMDocument, %s given', + VmDom::typeLabel($documentVar) + )); + } + $document = $documentVar->toObject(); + if (!VmDom::isDocument($document)) { + throw new \TypeError(sprintf( + 'DOMXPath::__construct(): Argument #1 ($document) must be of type DOMDocument, %s given', + $document->class->name + )); + } + VmDom::ensureDocument($document); + if (DomRegistry::has($receiver)) { + $state = DomRegistry::state($receiver); + $state->xpathDocumentId = $document->id; + + return; + } + $state = new DomNodeState(); + $state->nodeType = DomConstants::XML_XPATH; + $state->nodeName = 'DOMXPath'; + $state->xpathDocumentId = $document->id; + DomRegistry::attach($receiver, $state); + } +} diff --git a/ext/dom/XPathEvaluate.php b/ext/dom/XPathEvaluate.php new file mode 100644 index 000000000..dd0608653 --- /dev/null +++ b/ext/dom/XPathEvaluate.php @@ -0,0 +1,106 @@ +xpathReceiver($frame, 'DOMXPath::evaluate()'); + if (\count($frame->calledArgs) < 2) { + throw new \LogicException('DOMXPath::evaluate() expects at least 1 argument'); + } + $expression = $this->stringArg($frame->calledArgs[1], 'DOMXPath::evaluate()', 0); + $context = $this->optionalDomNodeArg($frame, 2, 'DOMXPath::evaluate()', 1); + $registerNodeNS = $this->optionalBoolArg($frame, 3, 'DOMXPath::evaluate()', 2); + if (null === $frame->vmContext) { + throw new \LogicException('DOMXPath::evaluate() requires VM context in this compiler build'); + } + $result = VmDomXPath::evaluate( + $frame->vmContext, + $xpath, + $expression, + $context, + $registerNodeNS + ); + if (null !== $frame->returnVar) { + $frame->returnVar->copyFrom($result); + } + } + + private function xpathReceiver(Frame $frame, string $label): \PHPCompiler\VM\ObjectEntry + { + if (\count($frame->calledArgs) < 1) { + throw new \LogicException($label.' called without $this'); + } + $var = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError(sprintf('%s must be called on an object, %s given', $label, VmDom::typeLabel($var))); + } + $object = $var->toObject(); + if (!VmDom::isXPath($object)) { + throw new \TypeError(sprintf('%s must be called on a DOMXPath instance', $label)); + } + + return $object; + } + + private function optionalDomNodeArg(Frame $frame, int $argIndex, string $label, int $paramIndex): ?\PHPCompiler\VM\ObjectEntry + { + if (!isset($frame->calledArgs[$argIndex])) { + return null; + } + $var = $frame->calledArgs[$argIndex]->resolveIndirect(); + if (Variable::TYPE_NULL === $var->type) { + return null; + } + if (Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError(sprintf( + '%s expects argument #%d to be of type ?DOMNode, %s given', + $label, + $paramIndex + 1, + VmDom::typeLabel($var) + )); + } + $object = $var->toObject(); + if (!VmDom::isDomNode($object)) { + throw new \TypeError(sprintf( + '%s expects argument #%d to be of type ?DOMNode, %s given', + $label, + $paramIndex + 1, + $object->class->name + )); + } + + return $object; + } + + private function optionalBoolArg(Frame $frame, int $argIndex, string $label, int $paramIndex): bool + { + if (!isset($frame->calledArgs[$argIndex])) { + return false; + } + $var = $frame->calledArgs[$argIndex]->resolveIndirect(); + if (Variable::TYPE_BOOL !== $var->type) { + throw new \TypeError(sprintf( + '%s expects argument #%d to be of type bool, %s given', + $label, + $paramIndex + 1, + VmDom::typeLabel($var) + )); + } + + return $var->bool; + } +} diff --git a/ext/dom/XPathQuery.php b/ext/dom/XPathQuery.php new file mode 100644 index 000000000..8588737a7 --- /dev/null +++ b/ext/dom/XPathQuery.php @@ -0,0 +1,92 @@ +xpathReceiver($frame, 'DOMXPath::query()'); + if (\count($frame->calledArgs) < 2) { + throw new \LogicException('DOMXPath::query() expects at least 1 argument'); + } + $expression = $this->stringArg($frame->calledArgs[1], 'DOMXPath::query()', 0); + $context = null; + if (isset($frame->calledArgs[2])) { + $context = $this->optionalDomNodeArg($frame->calledArgs[2], 'DOMXPath::query()', 1); + } + $registerNodeNS = false; + if (isset($frame->calledArgs[3])) { + $registerVar = $frame->calledArgs[3]->resolveIndirect(); + if (Variable::TYPE_BOOL !== $registerVar->type) { + throw new \TypeError(sprintf( + 'DOMXPath::query(): Argument #3 ($registerNodeNS) must be of type bool, %s given', + VmDom::typeLabel($registerVar) + )); + } + $registerNodeNS = $registerVar->bool; + } + if (null === $frame->vmContext) { + throw new \LogicException('DOMXPath::query() requires VM context in this compiler build'); + } + $list = VmDomXPath::query($frame->vmContext, $xpath, $expression, $context, $registerNodeNS); + if (null !== $frame->returnVar) { + $frame->returnVar->copyFrom($list); + } + } + + private function xpathReceiver(Frame $frame, string $label): \PHPCompiler\VM\ObjectEntry + { + if (\count($frame->calledArgs) < 1) { + throw new \LogicException($label.' called without $this'); + } + $var = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError(sprintf('%s must be called on an object, %s given', $label, VmDom::typeLabel($var))); + } + $object = $var->toObject(); + if (!VmDom::isXPath($object)) { + throw new \TypeError(sprintf('%s must be called on a DOMXPath instance', $label)); + } + + return $object; + } + + private function optionalDomNodeArg(Variable $var, string $label, int $index): ?\PHPCompiler\VM\ObjectEntry + { + $var = $var->resolveIndirect(); + if (Variable::TYPE_NULL === $var->type) { + return null; + } + if (Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError(sprintf( + '%s expects argument #%d to be of type ?DOMNode, %s given', + $label, + $index + 1, + VmDom::typeLabel($var) + )); + } + $object = $var->toObject(); + if (!VmDom::isDomNode($object)) { + throw new \TypeError(sprintf( + '%s expects argument #%d to be of type ?DOMNode, %s given', + $label, + $index + 1, + $object->class->name + )); + } + + return $object; + } +} diff --git a/ext/dom/XPathRegisterNamespace.php b/ext/dom/XPathRegisterNamespace.php new file mode 100644 index 000000000..efa33b6ce --- /dev/null +++ b/ext/dom/XPathRegisterNamespace.php @@ -0,0 +1,47 @@ +xpathReceiver($frame, 'DOMXPath::registerNamespace()'); + if (\count($frame->calledArgs) < 3) { + throw new \LogicException('DOMXPath::registerNamespace() expects at least 2 arguments'); + } + $prefix = $this->stringArg($frame->calledArgs[1], 'DOMXPath::registerNamespace()', 0); + $namespaceUri = $this->stringArg($frame->calledArgs[2], 'DOMXPath::registerNamespace()', 1); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool(VmDomXPath::registerNamespace($xpath, $prefix, $namespaceUri)); + } + + private function xpathReceiver(Frame $frame, string $label): \PHPCompiler\VM\ObjectEntry + { + if (\count($frame->calledArgs) < 1) { + throw new \LogicException($label.' called without $this'); + } + $var = $frame->calledArgs[0]->resolveIndirect(); + if (\PHPCompiler\VM\Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError(sprintf('%s must be called on an object, %s given', $label, VmDom::typeLabel($var))); + } + $object = $var->toObject(); + if (!VmDom::isXPath($object)) { + throw new \TypeError(sprintf('%s must be called on a DOMXPath instance', $label)); + } + + return $object; + } +} diff --git a/test/compliance/cases/stdlib/dom_xpath_query.phpt b/test/compliance/cases/stdlib/dom_xpath_query.phpt new file mode 100644 index 000000000..7b321fae3 --- /dev/null +++ b/test/compliance/cases/stdlib/dom_xpath_query.phpt @@ -0,0 +1,17 @@ +--TEST-- +stdlib DOMXPath::query() descendant attribute predicate (#6066, ext/dom/xpath.c) +--FILE-- +ab'; +$doc = new DOMDocument(); +$doc->loadXML($xml); +echo (int) class_exists('DOMXPath', false), "\n"; +$xpath = new DOMXPath($doc); +$nodes = $xpath->query('//item[@id="2"]'); +echo $nodes->length, "\n"; +echo $nodes->item(0)->textContent, "\n"; +?> +--EXPECT-- +1 +1 +b diff --git a/test/repro-maintainer/parity_domxpath.php b/test/repro-maintainer/parity_domxpath.php new file mode 100644 index 000000000..072cca658 --- /dev/null +++ b/test/repro-maintainer/parity_domxpath.php @@ -0,0 +1,23 @@ +ab'; +$doc = new DOMDocument(); +$doc->loadXML($xml); +if (!class_exists('DOMXPath', false)) { + fwrite(STDERR, "fail: DOMXPath class missing\n"); + exit(1); +} +$xpath = new DOMXPath($doc); +$nodes = $xpath->query('//item[@id="2"]'); +if (1 !== $nodes->length) { + fwrite(STDERR, "fail: expected 1 node, got ".$nodes->length."\n"); + exit(1); +} +if ('b' !== $nodes->item(0)->textContent) { + fwrite(STDERR, "fail: expected textContent b, got ".$nodes->item(0)->textContent."\n"); + exit(1); +} + +echo "ok\n"; diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index 3418da943..68090b9ad 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -4310,6 +4310,11 @@ require_once __DIR__.'/../../../ext/dom/TokenListReplace.php'; require_once __DIR__.'/../../../ext/dom/TokenListSupports.php'; require_once __DIR__.'/../../../ext/dom/TokenListToggle.php'; +require_once __DIR__.'/../../../ext/dom/VmDomXPath.php'; +require_once __DIR__.'/../../../ext/dom/XPathConstruct.php'; +require_once __DIR__.'/../../../ext/dom/XPathEvaluate.php'; +require_once __DIR__.'/../../../ext/dom/XPathQuery.php'; +require_once __DIR__.'/../../../ext/dom/XPathRegisterNamespace.php'; require_once __DIR__.'/../../../ext/dom/VmDomTokenList.php'; require_once __DIR__.'/../../../ext/dom/VmDomInstanceInvoke.php'; require_once __DIR__.'/../../../ext/dom/VmDomJitDispatch.php'; diff --git a/test/unit/DomModuleTest.php b/test/unit/DomModuleTest.php index e40b02250..521d46ad6 100644 --- a/test/unit/DomModuleTest.php +++ b/test/unit/DomModuleTest.php @@ -493,6 +493,25 @@ public function test_dom_text_split_text(): void self::assertSame("he\nllo\nprev\nnext\nhello\n", ob_get_clean()); } + public function test_dom_xpath_query_attribute_predicate(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +loadXML('ab'); +echo (int) class_exists('DOMXPath', false), "\n"; +$xpath = new DOMXPath($doc); +$nodes = $xpath->query('//item[@id="2"]'); +echo $nodes->length, "\n"; +echo $nodes->item(0)->textContent, "\n"; +PHP; + $block = $runtime->parseAndCompile($code, 'dom_xpath_query.php'); + ob_start(); + $runtime->run($block); + self::assertSame("1\n1\nb\n", ob_get_clean()); + } + public function test_runtime_shrink_has_no_dom_c_runtime(): void { $linker = (string) file_get_contents(__DIR__.'/../../lib/AOT/Linker.php');