@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Support PHP-Parser in the PHPAST application

Summary:
The PHPAST application allows viewing the xhpast token stream and AST.
It now also supports PHP-Parser generated ASTs and token streams.

Ref T16289

Test Plan:
* Apply D26522
* Enable prototypes via http://phorge.localhost/config/edit/phabricator.show-prototypes/
* Run `./bin/storage upgrade` to apply the database change
* Go to http://phorge.localhost/xhpast/ and click on the new "Use PHPAST" button in the top right
* Enter some PHP code
* Observe output and compare by doing the same on http://phorge.localhost/xhpast/
* After finishing testing this patch, `DROP TABLE {$NAMESPACE}_xhpast.phpast_parsetree; DELETE FROM phabricator_meta_data.patch_status WHERE patch = "phabricator:20251124.phpast.parsetree.sql";`

Reviewers: O1 Blessed Committers, aklapper

Reviewed By: O1 Blessed Committers, aklapper

Subscribers: aklapper, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno

Maniphest Tasks: T16289

Differential Revision: https://we.phorge.it/D26523

+406 -1
+10
resources/sql/autopatches/20251124.phpast.parsetree.sql
··· 1 + CREATE TABLE {$NAMESPACE}_xhpast.phpast_parsetree ( 2 + `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 3 + `authorPHID` VARBINARY(64) DEFAULT NULL, 4 + `input` LONGTEXT NOT NULL, 5 + `error` LONGTEXT DEFAULT NULL, 6 + `tokenStream` LONGBLOB NOT NULL, 7 + `tree` LONGTEXT NOT NULL, 8 + `dateCreated` INT UNSIGNED NOT NULL, 9 + `dateModified` INT UNSIGNED NOT NULL 10 + );
+16
src/__phutil_library_map__.php
··· 5413 5413 'PhorgeCodeWarningSetupCheck' => 'applications/config/check/PhorgeCodeWarningSetupCheck.php', 5414 5414 'PhorgeFlagFlaggedObjectCustomField' => 'applications/flag/customfield/PhorgeFlagFlaggedObjectCustomField.php', 5415 5415 'PhorgeFlagFlaggedObjectFieldStorage' => 'applications/flag/customfield/PhorgeFlagFlaggedObjectFieldStorage.php', 5416 + 'PhorgePHPASTParseTree' => 'applications/phpast/storage/PhorgePHPASTParseTree.php', 5417 + 'PhorgePHPASTViewFrameController' => 'applications/phpast/controller/PhorgePHPASTViewFrameController.php', 5418 + 'PhorgePHPASTViewFramesetController' => 'applications/phpast/controller/PhorgePHPASTViewFramesetController.php', 5419 + 'PhorgePHPASTViewInputController' => 'applications/phpast/controller/PhorgePHPASTViewInputController.php', 5420 + 'PhorgePHPASTViewPanelController' => 'applications/phpast/controller/PhorgePHPASTViewPanelController.php', 5421 + 'PhorgePHPASTViewRunController' => 'applications/phpast/controller/PhorgePHPASTViewRunController.php', 5422 + 'PhorgePHPASTViewStreamController' => 'applications/phpast/controller/PhorgePHPASTViewStreamController.php', 5423 + 'PhorgePHPASTViewTreeController' => 'applications/phpast/controller/PhorgePHPASTViewTreeController.php', 5416 5424 'PhorgeSystemDeprecationWarningListener' => 'applications/system/events/PhorgeSystemDeprecationWarningListener.php', 5417 5425 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 5418 5426 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', ··· 12277 12285 'PhorgeCodeWarningSetupCheck' => 'PhabricatorSetupCheck', 12278 12286 'PhorgeFlagFlaggedObjectCustomField' => 'PhabricatorCustomField', 12279 12287 'PhorgeFlagFlaggedObjectFieldStorage' => 'Phobject', 12288 + 'PhorgePHPASTParseTree' => 'PhabricatorXHPASTDAO', 12289 + 'PhorgePHPASTViewFrameController' => 'PhabricatorXHPASTViewController', 12290 + 'PhorgePHPASTViewFramesetController' => 'PhabricatorXHPASTViewController', 12291 + 'PhorgePHPASTViewInputController' => 'PhorgePHPASTViewPanelController', 12292 + 'PhorgePHPASTViewPanelController' => 'PhabricatorXHPASTViewController', 12293 + 'PhorgePHPASTViewRunController' => 'PhabricatorXHPASTViewController', 12294 + 'PhorgePHPASTViewStreamController' => 'PhorgePHPASTViewPanelController', 12295 + 'PhorgePHPASTViewTreeController' => 'PhorgePHPASTViewPanelController', 12280 12296 'PhorgeSystemDeprecationWarningListener' => 'PhabricatorEventListener', 12281 12297 'PhortuneAccount' => array( 12282 12298 'PhortuneDAO',
+13
src/applications/phpast/application/PhabricatorPHPASTApplication.php
··· 41 41 'stream/(?P<id>[1-9]\d*)/' 42 42 => 'PhabricatorXHPASTViewStreamController', 43 43 ), 44 + '/phpast/' => array( 45 + '' => PhorgePHPASTViewRunController::class, 46 + 'view/(?P<id>[1-9]\d*)/' 47 + => PhorgePHPASTViewFrameController::class, 48 + 'frameset/(?P<id>[1-9]\d*)/' 49 + => PhorgePHPASTViewFramesetController::class, 50 + 'input/(?P<id>[1-9]\d*)/' 51 + => PhorgePHPASTViewInputController::class, 52 + 'tree/(?P<id>[1-9]\d*)/' 53 + => PhorgePHPASTViewTreeController::class, 54 + 'stream/(?P<id>[1-9]\d*)/' 55 + => PhorgePHPASTViewStreamController::class, 56 + ), 44 57 ); 45 58 } 46 59
+17 -1
src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php
··· 3 3 final class PhabricatorXHPASTViewRunController 4 4 extends PhabricatorXHPASTViewController { 5 5 6 + protected function buildApplicationCrumbs() { 7 + return parent::buildApplicationCrumbs() 8 + ->addAction( 9 + id(new PHUIListItemView()) 10 + ->setName(pht('Use PHPAST')) 11 + ->setHref('/phpast/') 12 + ->setIcon('fa-random')); 13 + } 14 + 6 15 public function handleRequest(AphrontRequest $request) { 7 16 $viewer = $this->getViewer(); 8 17 ··· 34 43 } 35 44 36 45 $form = id(new AphrontFormView()) 37 - ->setUser($viewer) 46 + ->setViewer($viewer) 38 47 ->appendChild( 39 48 id(new AphrontFormTextAreaControl()) 40 49 ->setLabel(pht('Source')) ··· 63 72 64 73 return $this->newPage() 65 74 ->setTitle($title) 75 + ->setCrumbs( 76 + id(new PHUICrumbsView()) 77 + ->addAction( 78 + id(new PHUIListItemView()) 79 + ->setName(pht('Use PHPAST')) 80 + ->setHref('/phpast/') 81 + ->setIcon('fa-random'))) 66 82 ->appendChild($view); 67 83 68 84 }
+27
src/applications/phpast/controller/PhorgePHPASTViewFrameController.php
··· 1 + <?php 2 + 3 + final class PhorgePHPASTViewFrameController 4 + extends PhabricatorXHPASTViewController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $id = $request->getURIData('id'); 12 + 13 + return $this->newPage() 14 + ->setApplicationName('PHPASTView') 15 + ->setBaseURI('/phpast/') 16 + ->setTitle(pht('PHPAST View')) 17 + ->setGlyph("\xE2\x96\xA0") 18 + ->appendChild(phutil_tag( 19 + 'iframe', 20 + array( 21 + 'src' => "/phpast/frameset/{$id}/", 22 + 'frameborder' => '0', 23 + 'style' => 'width: 100%; height: 800px;', 24 + '', 25 + ))); 26 + } 27 + }
+24
src/applications/phpast/controller/PhorgePHPASTViewFramesetController.php
··· 1 + <?php 2 + 3 + final class PhorgePHPASTViewFramesetController 4 + extends PhabricatorXHPASTViewController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $id = $request->getURIData('id'); 12 + 13 + return id(new AphrontWebpageResponse()) 14 + ->setFrameable(true) 15 + ->setContent(phutil_tag( 16 + 'frameset', 17 + array('cols' => '33%, 34%, 33%'), 18 + array( 19 + phutil_tag('frame', array('src' => "/phpast/input/{$id}/")), 20 + phutil_tag('frame', array('src' => "/phpast/tree/{$id}/")), 21 + phutil_tag('frame', array('src' => "/phpast/stream/{$id}/")), 22 + ))); 23 + } 24 + }
+10
src/applications/phpast/controller/PhorgePHPASTViewInputController.php
··· 1 + <?php 2 + 3 + final class PhorgePHPASTViewInputController 4 + extends PhorgePHPASTViewPanelController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $input = $this->getStorageTree()->getInput(); 8 + return $this->buildPHPASTViewPanelResponse($input); 9 + } 10 + }
+77
src/applications/phpast/controller/PhorgePHPASTViewPanelController.php
··· 1 + <?php 2 + 3 + abstract class PhorgePHPASTViewPanelController 4 + extends PhabricatorXHPASTViewController { 5 + 6 + private $id; 7 + private $storageTree; 8 + 9 + public function shouldAllowPublic() { 10 + return true; 11 + } 12 + 13 + public function willProcessRequest(array $data) { 14 + // Load the parser now to ensure the autoloader for PHP-Parser is available. 15 + PhutilPHPParserLibrary::getParser(); 16 + 17 + $this->id = $data['id']; 18 + $this->storageTree = id(new PhorgePHPASTParseTree()) 19 + ->load($this->id); 20 + 21 + if (!$this->storageTree) { 22 + throw new Exception(pht('No such AST!')); 23 + } 24 + } 25 + 26 + protected function getStorageTree() { 27 + return $this->storageTree; 28 + } 29 + 30 + protected function buildPHPASTViewPanelResponse($content) { 31 + $content = hsprintf( 32 + '<!DOCTYPE html>'. 33 + '<html>'. 34 + '<head>'. 35 + '<style> 36 + body { 37 + white-space: pre; 38 + font: 10px "Monaco"; 39 + cursor: pointer; 40 + } 41 + 42 + .token { 43 + padding: 2px 4px; 44 + margin: 2px 2px; 45 + border: 1px solid #bbbbbb; 46 + line-height: 24px; 47 + } 48 + 49 + ul { 50 + margin: 0 0 0 1em; 51 + padding: 0; 52 + list-style: none; 53 + line-height: 1em; 54 + } 55 + 56 + li { 57 + margin: 0; 58 + padding: 0; 59 + } 60 + 61 + li span { 62 + background: #dddddd; 63 + padding: 3px 6px; 64 + } 65 + 66 + </style>'. 67 + '</head>'. 68 + '<body>%s</body>'. 69 + '</html>', 70 + $content); 71 + 72 + return id(new AphrontWebpageResponse()) 73 + ->setFrameable(true) 74 + ->setContent($content); 75 + } 76 + 77 + }
+81
src/applications/phpast/controller/PhorgePHPASTViewRunController.php
··· 1 + <?php 2 + 3 + /** 4 + * @phutil-external-symbol class PhpParser\Error 5 + */ 6 + final class PhorgePHPASTViewRunController 7 + extends PhabricatorXHPASTViewController { 8 + 9 + public function handleRequest(AphrontRequest $request) { 10 + $viewer = $this->getViewer(); 11 + 12 + if ($request->isFormPost()) { 13 + $source = $request->getStr('source'); 14 + 15 + $storage_tree = id(new PhorgePHPASTParseTree()) 16 + ->setInput($source) 17 + ->setAuthorPHID($viewer->getPHID()); 18 + 19 + $parser = PhutilPHPParserLibrary::getParser(); 20 + $exposes_token_stream = version_compare( 21 + PhutilPHPParserLibrary::getVersion(), 22 + PhutilPHPParserLibrary::EXPECTED_VERSION, 23 + '>='); 24 + 25 + try { 26 + $storage_tree->setTree($parser->parse($source)); 27 + if ($exposes_token_stream) { 28 + $storage_tree->setTokenStream($parser->getTokens()); 29 + } 30 + } catch (PhpParser\Error $ex) { 31 + $storage_tree->setError($ex->getMessageWithColumnInfo($source)); 32 + } 33 + 34 + $storage_tree->save(); 35 + 36 + return id(new AphrontRedirectResponse()) 37 + ->setURI('/phpast/view/'.$storage_tree->getID().'/'); 38 + } 39 + 40 + $form = id(new AphrontFormView()) 41 + ->setViewer($viewer) 42 + ->appendChild( 43 + id(new AphrontFormTextAreaControl()) 44 + ->setLabel(pht('Source')) 45 + ->setName('source') 46 + ->setValue("<?php\n\n") 47 + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)) 48 + ->appendChild( 49 + id(new AphrontFormSubmitControl()) 50 + ->setValue(pht('Parse'))); 51 + 52 + $form_box = id(new PHUIObjectBoxView()) 53 + ->setHeaderText(pht('Generate PHP AST')) 54 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 55 + ->setForm($form); 56 + 57 + $title = pht('PHPAST View'); 58 + $header = id(new PHUIHeaderView()) 59 + ->setHeader($title) 60 + ->setHeaderIcon('fa-ambulance'); 61 + 62 + $view = id(new PHUITwoColumnView()) 63 + ->setHeader($header) 64 + ->setFooter(array( 65 + $form_box, 66 + )); 67 + 68 + return $this->newPage() 69 + ->setTitle($title) 70 + ->setCrumbs( 71 + id(new PHUICrumbsView()) 72 + ->addAction( 73 + id(new PHUIListItemView()) 74 + ->setName(pht('Use XHPAST')) 75 + ->setHref('/xhpast/') 76 + ->setIcon('fa-random'))) 77 + ->appendChild($view); 78 + 79 + } 80 + 81 + }
+33
src/applications/phpast/controller/PhorgePHPASTViewStreamController.php
··· 1 + <?php 2 + 3 + final class PhorgePHPASTViewStreamController 4 + extends PhorgePHPASTViewPanelController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $storage = $this->getStorageTree(); 8 + $err = $storage->getError(); 9 + $token_stream = $storage->getTokenStream(); 10 + 11 + if ($err) { 12 + return $this->buildPHPASTViewPanelResponse($err); 13 + } 14 + 15 + $tokens = array(); 16 + foreach ($token_stream as $id => $token) { 17 + $seq = $id; 18 + $name = $token->getTokenName(); 19 + $title = pht('Token %d: %s', $seq, $name); 20 + 21 + $tokens[] = phutil_tag( 22 + 'span', 23 + array( 24 + 'title' => $title, 25 + 'class' => 'token', 26 + ), 27 + $token->text); 28 + } 29 + 30 + return $this->buildPHPASTViewPanelResponse( 31 + phutil_implode_html('', $tokens)); 32 + } 33 + }
+52
src/applications/phpast/controller/PhorgePHPASTViewTreeController.php
··· 1 + <?php 2 + 3 + /** 4 + * @phutil-external-symbol class PhpParser\Node 5 + */ 6 + final class PhorgePHPASTViewTreeController 7 + extends PhorgePHPASTViewPanelController { 8 + 9 + public function handleRequest(AphrontRequest $request) { 10 + $storage = $this->getStorageTree(); 11 + $err = $storage->getError(); 12 + $ast = $storage->getTree(); 13 + 14 + if ($err) { 15 + return $this->buildPHPASTViewPanelResponse($err); 16 + } 17 + 18 + return $this->buildPHPASTViewPanelResponse($this->buildTree($ast)); 19 + } 20 + 21 + protected function buildTree(array $nodes) { 22 + $tree = array(); 23 + 24 + foreach ($nodes as $node) { 25 + $tree[] = phutil_tag( 26 + 'li', 27 + array(), 28 + phutil_tag( 29 + 'span', 30 + array( 31 + 'title' => $node->getType(), 32 + ), 33 + $node->getType())); 34 + 35 + foreach ($node->getSubNodeNames() as $sub_node_name) { 36 + $sub_node = $node->{$sub_node_name}; 37 + 38 + if (is_array($sub_node) && $sub_node) { 39 + $tree[] = $this->buildTree($sub_node); 40 + } else if ($sub_node instanceof PhpParser\Node) { 41 + $tree[] = $this->buildTree(array($sub_node)); 42 + } 43 + } 44 + } 45 + 46 + return phutil_tag( 47 + 'ul', 48 + array(), 49 + phutil_implode_html("\n", $tree)); 50 + } 51 + 52 + }
+46
src/applications/phpast/storage/PhorgePHPASTParseTree.php
··· 1 + <?php 2 + 3 + /** 4 + * @phutil-external-symbol class PhpParser\JsonDecoder 5 + */ 6 + final class PhorgePHPASTParseTree extends PhabricatorXHPASTDAO { 7 + 8 + protected $authorPHID; 9 + protected $input; 10 + protected $tree; 11 + protected $error; 12 + protected $tokenStream; 13 + 14 + protected function getConfiguration() { 15 + return array( 16 + self::CONFIG_SERIALIZATION => array( 17 + 'tokenStream' => self::SERIALIZATION_PHP, 18 + 'tree' => self::SERIALIZATION_JSON, 19 + ), 20 + self::CONFIG_BINARY => array( 21 + 'tokenStream' => true, 22 + ), 23 + self::CONFIG_COLUMN_SCHEMA => array( 24 + 'authorPHID' => 'phid?', 25 + 'error' => 'text?', 26 + 'input' => 'text', 27 + ), 28 + ) + parent::getConfiguration(); 29 + } 30 + 31 + protected function applyLiskDataSerialization(array &$data, $deserialize) { 32 + // applyLiskDataSerialization overwrites $data, so capture the 33 + // JSON before it does so. 34 + $tree = $data['tree']; 35 + 36 + parent::applyLiskDataSerialization($data, $deserialize); 37 + 38 + if ($deserialize) { 39 + $data['tree'] = id(new PhpParser\JsonDecoder())->decode($tree); 40 + } 41 + } 42 + 43 + public function getTableName() { 44 + return 'phpast_parsetree'; 45 + } 46 + }