From aa11ee47f63d39d741b8d6989eacdbd896725c3c Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobev Date: Tue, 31 Dec 2024 15:26:51 +0300 Subject: [PATCH 1/4] WIP: migrate to single .phar for php utils --- .gitignore | 5 + packages/language-server/phpUtils/.gitignore | 3 + .../phpUtils/completeClassPsr4.php | 53 - .../language-server/phpUtils/composer.json | 26 + .../language-server/phpUtils/composer.lock | 1680 +++++++++++++++++ .../phpUtils/definitionClassPsr4.php | 23 - .../phpUtils/getTwigMetadata.php | 135 -- packages/language-server/phpUtils/index.php | 28 + .../phpUtils/printCraftTwigEnvironment.php | 27 - .../phpUtils/printTwigEnvironment.php | 15 - .../language-server/phpUtils/reflectType.php | 97 - .../language-server/phpUtils/src/Metadata.php | 134 ++ .../phpUtils/src/Reflection.php | 222 +++ .../phpUtils/src/TwigUtils.php | 161 ++ .../src/configuration/ConfigurationManager.ts | 7 +- .../configuration/LanguageServerSettings.ts | 1 + .../language-server/src/definitions/index.ts | 4 +- .../src/phpInterop/IPhpExecutor.ts | 4 + .../src/phpInterop/PhpExecutor.ts | 104 +- .../twigEnvironment/CraftTwigEnvironment.ts | 18 +- .../src/twigEnvironment/PhpUtilPath.ts | 6 +- .../twigEnvironment/SymfonyTwigEnvironment.ts | 4 +- .../twigEnvironment/VanillaTwigEnvironment.ts | 24 +- .../src/utils/node/parseFunctionCall.ts | 5 +- packages/vscode/build/index.mjs | 3 +- packages/vscode/package.json | 6 + 26 files changed, 2389 insertions(+), 406 deletions(-) create mode 100644 packages/language-server/phpUtils/.gitignore delete mode 100644 packages/language-server/phpUtils/completeClassPsr4.php create mode 100644 packages/language-server/phpUtils/composer.json create mode 100644 packages/language-server/phpUtils/composer.lock delete mode 100644 packages/language-server/phpUtils/definitionClassPsr4.php delete mode 100755 packages/language-server/phpUtils/getTwigMetadata.php create mode 100644 packages/language-server/phpUtils/index.php delete mode 100755 packages/language-server/phpUtils/printCraftTwigEnvironment.php delete mode 100644 packages/language-server/phpUtils/printTwigEnvironment.php delete mode 100644 packages/language-server/phpUtils/reflectType.php create mode 100644 packages/language-server/phpUtils/src/Metadata.php create mode 100644 packages/language-server/phpUtils/src/Reflection.php create mode 100644 packages/language-server/phpUtils/src/TwigUtils.php diff --git a/.gitignore b/.gitignore index b335a3e..9ad3de3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ packages/vscode/dist packages/language-server/dist .vscode/settings.json *.vsix + +pyproject.toml +requirements.lock +requirements-dev.lock +.python-version diff --git a/packages/language-server/phpUtils/.gitignore b/packages/language-server/phpUtils/.gitignore new file mode 100644 index 0000000..1239218 --- /dev/null +++ b/packages/language-server/phpUtils/.gitignore @@ -0,0 +1,3 @@ +vendor/ +# remove compiled artefact from version control +twiggy-php-utils.phar diff --git a/packages/language-server/phpUtils/completeClassPsr4.php b/packages/language-server/phpUtils/completeClassPsr4.php deleted file mode 100644 index 6703476..0000000 --- a/packages/language-server/phpUtils/completeClassPsr4.php +++ /dev/null @@ -1,53 +0,0 @@ -getPrefixesPsr4(); -$prefixesPsr4 = array_keys($prefixesPsr4ToPaths); - -if (!$NAMESPACE_PSR4) { - echo json_encode($prefixesPsr4, JSON_PRETTY_PRINT) . PHP_EOL; - exit(0); -} - -/** @var array $classesInNamespace */ -$classesInNamespace = []; -$namespaceFirstPart = explode('\\', $NAMESPACE_PSR4)[0]; - -foreach ($prefixesPsr4 as $prefix) { - if (!str_starts_with($prefix, $namespaceFirstPart)) { - continue; - } - - $dir = $prefixesPsr4ToPaths[$prefix][0]; - $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); - - foreach ($iterator as $file) { - if ($file->isDir()) continue; - if (pathinfo($file->getFilename(), PATHINFO_EXTENSION) === 'php') { - include_once $file->getPathname(); - } - } - - foreach (get_declared_classes() as $class) { - if (!str_starts_with($class, $NAMESPACE_PSR4)) { - continue; - } - - $classesInNamespace[$class] = true; - } -} - -echo json_encode(array_keys($classesInNamespace), JSON_PRETTY_PRINT) . PHP_EOL; diff --git a/packages/language-server/phpUtils/composer.json b/packages/language-server/phpUtils/composer.json new file mode 100644 index 0000000..8f77a99 --- /dev/null +++ b/packages/language-server/phpUtils/composer.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://getcomposer.org/schema.json", + "name": "moetelo/twiggy-php-utils", + "description": "PHP utilities for Twig Language Server", + "type": "project", + "license": "Mozilla Public License 2.0", + "autoload": { + "psr-4": { + "Twiggy\\": "src/" + } + }, + "authors": [ + { + "name": "Mikhail Gunin", + "email": "gunka462@gmail.com" + } + ], + "minimum-stability": "dev", + "require-dev": { + "clue/phar-composer": "1.x-dev", + "twig/twig": "4.x-dev" + }, + "scripts": { + "build": "vendor/bin/phar-composer build ." + } +} diff --git a/packages/language-server/phpUtils/composer.lock b/packages/language-server/phpUtils/composer.lock new file mode 100644 index 0000000..b775193 --- /dev/null +++ b/packages/language-server/phpUtils/composer.lock @@ -0,0 +1,1680 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "891084cfcd15fcbcff76c629498e632a", + "packages": [], + "packages-dev": [ + { + "name": "clue/phar-composer", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/clue/phar-composer.git", + "reference": "9508b6db07b60c0cb7310a496692ba2d472abcc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/phar-composer/zipball/9508b6db07b60c0cb7310a496692ba2d472abcc0", + "reference": "9508b6db07b60c0cb7310a496692ba2d472abcc0", + "shasum": "" + }, + "require": { + "knplabs/packagist-api": "^1.0", + "php": ">=5.3.6", + "symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.0 || ^2.5", + "symfony/finder": "^6.0 || ^5.0 || ^4.0 || ^3.0 || ^2.5", + "symfony/process": "^6.0 || ^5.0 || ^4.0 || ^3.0 || ^2.5" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36" + }, + "default-branch": true, + "bin": [ + "bin/phar-composer" + ], + "type": "library", + "autoload": { + "psr-4": { + "Clue\\PharComposer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Simple phar creation for any project managed via Composer", + "homepage": "https://github.com/clue/phar-composer", + "keywords": [ + "build process", + "bundle dependencies", + "composer", + "executable phar", + "phar" + ], + "support": { + "issues": "https://github.com/clue/phar-composer/issues", + "source": "https://github.com/clue/phar-composer/tree/1.x" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-11-04T08:44:23+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "f587d8c05c6e00f99cbfb32d565e4f6743c07ee4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/f587d8c05c6e00f99cbfb32d565e4f6743c07ee4", + "reference": "f587d8c05c6e00f99cbfb32d565e4f6743c07ee4", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T21:47:00+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "knplabs/packagist-api", + "version": "1.7.x-dev", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/packagist-api.git", + "reference": "4feae228a4505c1cd817da61e752e5dea2b22c2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/packagist-api/zipball/4feae228a4505c1cd817da61e752e5dea2b22c2d", + "reference": "4feae228a4505c1cd817da61e752e5dea2b22c2d", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0 || ^2.0", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Packagist\\Api\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com" + } + ], + "description": "Packagist API client.", + "homepage": "http://knplabs.com", + "keywords": [ + "api", + "composer", + "packagist" + ], + "support": { + "issues": "https://github.com/KnpLabs/packagist-api/issues", + "source": "https://github.com/KnpLabs/packagist-api/tree/1.7" + }, + "time": "2022-03-01T08:20:15+00:00" + }, + { + "name": "psr/container", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "707984727bd5b2b670e59559d3ed2500240cf875" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/707984727bd5b2b670e59559d3ed2500240cf875", + "reference": "707984727bd5b2b670e59559d3ed2500240cf875", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container" + }, + "time": "2023-09-22T11:11:30+00:00" + }, + { + "name": "psr/http-client", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/console", + "version": "6.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/6.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/main" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "6.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/6.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-29T13:51:37+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/1.x" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/process", + "version": "6.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/3cb242f059c14ae08591c5c4087d1fe443564392", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/6.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T14:19:14+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "5ad38698559cf88b6296629e19b15ef3239c9d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/5ad38698559cf88b6296629e19b15ef3239c9d7a", + "reference": "5ad38698559cf88b6296629e19b15ef3239c9d7a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "default-branch": true, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/main" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/string", + "version": "7.3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "twig/twig", + "version": "4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "63b86e30573b4e459aa540be2025fba343072f7b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/63b86e30573b4e459aa540be2025fba343072f7b", + "reference": "63b86e30573b4e459aa540be2025fba343072f7b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0@stable", + "phpunit/phpunit": "^11.4@stable", + "psr/container": "^1.0|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/4.x" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-12-12T09:28:23+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "clue/phar-composer": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/packages/language-server/phpUtils/definitionClassPsr4.php b/packages/language-server/phpUtils/definitionClassPsr4.php deleted file mode 100644 index 2c73cf4..0000000 --- a/packages/language-server/phpUtils/definitionClassPsr4.php +++ /dev/null @@ -1,23 +0,0 @@ -findFile($CLASS_PSR4) ?: null; - -$result = [ - 'path' => $filePath, -]; - -echo json_encode($result, JSON_PRETTY_PRINT) . PHP_EOL; diff --git a/packages/language-server/phpUtils/getTwigMetadata.php b/packages/language-server/phpUtils/getTwigMetadata.php deleted file mode 100755 index 2905d2d..0000000 --- a/packages/language-server/phpUtils/getTwigMetadata.php +++ /dev/null @@ -1,135 +0,0 @@ - 3 && ctype_alpha($file[0]) - && ':' === $file[1] - && strspn($file, '/\\', 2, 1) - ) - || null !== parse_url($file, \PHP_URL_SCHEME) - ; -} - -/** - * Map supported loader namespaces to paths. - * @param array &$loaderPaths - * @param LoaderInterface $loader Loader. - */ -function mapNamespaces(array &$loaderPaths, LoaderInterface $loader): void { - if ($loader instanceof \Twig\Loader\ChainLoader) { - foreach ($loader->getLoaders() as $subLoader) { - mapNamespaces($loaderPaths, $subLoader); - } - - return; - } - - if ($loader instanceof \Twig\Loader\FilesystemLoader) { - $namespaces = $loader->getNamespaces(); - $rootPath = getcwd() . \DIRECTORY_SEPARATOR; - - foreach ($namespaces as $namespace) { - $ns_index = \Twig\Loader\FilesystemLoader::MAIN_NAMESPACE === $namespace - ? '' - : ('@' . $namespace); - - $loaderPaths[$ns_index] = []; - foreach ($loader->getPaths($namespace) as $path) { - $loaderPaths[$ns_index][] = realpath( - isAbsolutePath($path) - ? $path - : $rootPath . $path - ); - } - } - } -} - -function getTwigMetadata(\Twig\Environment $twig, string $framework = ''): array { - $loaderPathsArray = []; - if ($framework !== 'craft') { - mapNamespaces($loaderPathsArray, $twig->getLoader()); - } - - $globals = $twig->getGlobals(); - $globalsArray = []; - $emptyArrayObject = new \ArrayObject(); - foreach ($globals as $key => $value) { - $globalsArray[$key] = is_scalar($value) ? $value : $emptyArrayObject; - } - - return [ - 'functions' => array_reduce( - $twig->getFunctions(), - fn ($acc, $item) => $acc + [$item->getName() => \Twiggy\Metadata\getArguments('functions', $item)], - [], - ), - 'filters' => array_reduce( - $twig->getFilters(), - fn ($acc, $item) => $acc + [$item->getName() => \Twiggy\Metadata\getArguments('filters', $item)], - [], - ), - 'tests' => array_values( - array_map( - fn ($item) => $item->getName(), - $twig->getTests(), - ), - ), - 'globals' => $globalsArray, - 'loader_paths' => $loaderPathsArray, - ]; -} - -// https://github.com/symfony/twig-bridge/blob/1d5745dac2e043553177a3b88a76b99c2a2f6c2e/Command/DebugCommand.php#L305-L361 -function getArguments(string $type, \Twig\TwigFunction|\Twig\TwigFilter $entity): mixed { - $cb = $entity->getCallable(); - if (null === $cb) { - return null; - } - if (\is_array($cb)) { - if (!method_exists($cb[0], $cb[1])) { - return null; - } - $refl = new \ReflectionMethod($cb[0], $cb[1]); - } elseif (\is_object($cb) && method_exists($cb, '__invoke')) { - $refl = new \ReflectionMethod($cb, '__invoke'); - } elseif (\function_exists($cb)) { - $refl = new \ReflectionFunction($cb); - } elseif (\is_string($cb) && preg_match('{^(.+)::(.+)$}', $cb, $m) && method_exists($m[1], $m[2])) { - $refl = new \ReflectionMethod($m[1], $m[2]); - } else { - throw new \UnexpectedValueException('Unsupported callback type.'); - } - - $args = $refl->getParameters(); - - // filter out context/environment args - if ($entity->needsEnvironment()) { - array_shift($args); - } - if ($entity->needsContext()) { - array_shift($args); - } - - if ('filters' === $type) { - // remove the value the filter is applied on - array_shift($args); - } - - // format args - $args = array_map(function (\ReflectionParameter $param) { - if ($param->isDefaultValueAvailable()) { - return $param->getName().' = '.json_encode($param->getDefaultValue()); - } - - return $param->getName(); - }, $args); - - return $args; -} diff --git a/packages/language-server/phpUtils/index.php b/packages/language-server/phpUtils/index.php new file mode 100644 index 0000000..686c08b --- /dev/null +++ b/packages/language-server/phpUtils/index.php @@ -0,0 +1,28 @@ + false, + 'result' => \Twiggy\TwigUtils::run($argv), + ]; + } catch (\Throwable $e) { + $result = [ + 'error' => true, + 'message' => $e->getMessage(), + ]; + fwrite($stderr, $e); + } + // direct stdout to stderr, so custom code wont mess up with JSON output + $stdout = ob_get_clean(); + if ($stdout) + fwrite($stderr, $stdout); + echo json_encode($result, JSON_PRETTY_PRINT) . PHP_EOL; +} finally { + fclose($stderr); +} diff --git a/packages/language-server/phpUtils/printCraftTwigEnvironment.php b/packages/language-server/phpUtils/printCraftTwigEnvironment.php deleted file mode 100755 index 7d4af48..0000000 --- a/packages/language-server/phpUtils/printCraftTwigEnvironment.php +++ /dev/null @@ -1,27 +0,0 @@ -getView(); -$twig = $view->getTwig(); -$templateRoots = $view->getSiteTemplateRoots(); - -$twigMetadata = \Twiggy\Metadata\getTwigMetadata($twig, 'craft'); -$twigMetadata['loader_paths'] = $view->getSiteTemplateRoots(); - -echo json_encode($twigMetadata, JSON_PRETTY_PRINT) . PHP_EOL; diff --git a/packages/language-server/phpUtils/printTwigEnvironment.php b/packages/language-server/phpUtils/printTwigEnvironment.php deleted file mode 100644 index 3c8f56a..0000000 --- a/packages/language-server/phpUtils/printTwigEnvironment.php +++ /dev/null @@ -1,15 +0,0 @@ -findFile($INSTANCE_CLASS); -require_once $phpFilePath; - -$refClass = new \ReflectionClass($INSTANCE_CLASS); - -$properties = $refClass->getProperties(\ReflectionProperty::IS_PUBLIC); -$methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC); - -const GETTER_PREFIX = 'get'; -function getPropertyName(string $getterName): string { - return lcfirst( - substr($getterName, strlen(GETTER_PREFIX)), - ); -} - -function typeToString(\ReflectionType $type): string { - if ($type instanceof \ReflectionNamedType) { - return $type->getName(); - } - - if ($type instanceof \ReflectionUnionType) { - return implode('|', array_map( - fn(\ReflectionNamedType $type) => $type->getName(), - $type->getTypes(), - )); - } - - if ($type instanceof \ReflectionIntersectionType) { - return implode('&', array_map( - fn(\ReflectionNamedType $type) => $type->getName(), - $type->getTypes(), - )); - } - - throw new \RuntimeException('Unknown type'); -} - -$completionProperties = []; -$completionMethods = []; -/** @var \ReflectionMethod $method */ -foreach ($methods as $method) { - if ($method->isConstructor() || $method->isDestructor()) { - continue; - } - - $methodName = $method->getName(); - if (str_starts_with($methodName, '__')) { - continue; - } - - $parameters = $method->getParameters(); - - if (str_starts_with($methodName, GETTER_PREFIX) && count($parameters) === 0) { - $propertyName = getPropertyName($methodName); - $completionProperties[] = [ - 'name' => $propertyName, - 'type' => $method->getReturnType()?->getName() ?? '', - ]; - } - - $completionMethods[] = [ - 'name' => $methodName, - 'type' => $method->hasReturnType() ? typeToString($method->getReturnType()) : '', - 'parameters' => array_map( - fn(\ReflectionParameter $parameter) => [ - 'name' => $parameter->getName(), - 'type' => $parameter->hasType() ? typeToString($parameter->getType()) : '', - 'isOptional' => $parameter->isOptional(), - 'isVariadic' => $parameter->isVariadic(), - ], - $parameters, - ), - ]; -} - -$result = [ - 'properties' => $completionProperties, - 'methods' => $completionMethods, -]; - -echo json_encode($result, JSON_PRETTY_PRINT) . PHP_EOL; diff --git a/packages/language-server/phpUtils/src/Metadata.php b/packages/language-server/phpUtils/src/Metadata.php new file mode 100644 index 0000000..560b1f4 --- /dev/null +++ b/packages/language-server/phpUtils/src/Metadata.php @@ -0,0 +1,134 @@ + 3 && ctype_alpha($file[0]) + && ':' === $file[1] + && strspn($file, '/\\', 2, 1) + ) + || null !== parse_url($file, \PHP_URL_SCHEME) + ; + } + + /** + * Map supported loader namespaces to paths. + * @param array &$loaderPaths Array where key is namespace and value is an array of FS paths. + * @param \Twig\Loader\LoaderInterface $loader Loader. + */ + public static function mapNamespaces(array &$loaderPaths, \Twig\Loader\LoaderInterface $loader): void { + if ($loader instanceof \Twig\Loader\ChainLoader) { + foreach ($loader->getLoaders() as $subLoader) { + static::mapNamespaces($loaderPaths, $subLoader); + } + + return; + } + + if ($loader instanceof \Twig\Loader\FilesystemLoader) { + $namespaces = $loader->getNamespaces(); + $rootPath = getcwd() . \DIRECTORY_SEPARATOR; + + foreach ($namespaces as $namespace) { + $ns_index = \Twig\Loader\FilesystemLoader::MAIN_NAMESPACE === $namespace + ? '' + : ('@' . $namespace); + + $loaderPaths[$ns_index] = []; + foreach ($loader->getPaths($namespace) as $path) { + $loaderPaths[$ns_index][] = realpath( + static::isAbsolutePath($path) + ? $path + : $rootPath . $path + ); + } + } + } + } + + public static function getTwigMetadata(\Twig\Environment $twig, bool $map_namespaces = true): array { + $loaderPathsArray = []; + if ($map_namespaces) { + static::mapNamespaces($loaderPathsArray, $twig->getLoader()); + } + + $globals = $twig->getGlobals(); + $globalsArray = []; + $emptyArrayObject = new \ArrayObject(); + foreach ($globals as $key => $value) { + $globalsArray[$key] = is_scalar($value) ? $value : $emptyArrayObject; + } + + return [ + 'functions' => array_reduce( + $twig->getFunctions(), + fn ($acc, $item) => $acc + [$item->getName() => static::getArguments('functions', $item)], + [], + ), + 'filters' => array_reduce( + $twig->getFilters(), + fn ($acc, $item) => $acc + [$item->getName() => static::getArguments('filters', $item)], + [], + ), + 'tests' => array_values( + array_map( + fn ($item) => $item->getName(), + $twig->getTests(), + ), + ), + 'globals' => $globalsArray, + 'loader_paths' => $loaderPathsArray, + ]; + } + + // https://github.com/symfony/twig-bridge/blob/1d5745dac2e043553177a3b88a76b99c2a2f6c2e/Command/DebugCommand.php#L305-L361 + public static function getArguments(string $type, \Twig\TwigFunction|\Twig\TwigFilter $entity): mixed { + $cb = $entity->getCallable(); + if (null === $cb) { + return null; + } + if (\is_array($cb)) { + if (!method_exists($cb[0], $cb[1])) { + return null; + } + $refl = new \ReflectionMethod($cb[0], $cb[1]); + } elseif (\is_object($cb) && method_exists($cb, '__invoke')) { + $refl = new \ReflectionMethod($cb, '__invoke'); + } elseif (\function_exists($cb)) { + $refl = new \ReflectionFunction($cb); + } elseif (\is_string($cb) && preg_match('{^(.+)::(.+)$}', $cb, $m) && method_exists($m[1], $m[2])) { + $refl = new \ReflectionMethod($m[1], $m[2]); + } else { + throw new \UnexpectedValueException('Unsupported callback type.'); + } + + $args = $refl->getParameters(); + + // filter out context/environment args + if ($entity->needsEnvironment()) { + array_shift($args); + } + if ($entity->needsContext()) { + array_shift($args); + } + + if ('filters' === $type) { + // remove the value the filter is applied on + array_shift($args); + } + + // format args + $args = array_map(function (\ReflectionParameter $param) { + if ($param->isDefaultValueAvailable()) { + return $param->getName().' = '.json_encode($param->getDefaultValue()); + } + + return $param->getName(); + }, $args); + + return $args; + } +} diff --git a/packages/language-server/phpUtils/src/Reflection.php b/packages/language-server/phpUtils/src/Reflection.php new file mode 100644 index 0000000..c7ba9bc --- /dev/null +++ b/packages/language-server/phpUtils/src/Reflection.php @@ -0,0 +1,222 @@ +findFile($type); + if ($phpFilePath) + return $phpFilePath; + + try { + return (new \ReflectionClass($type))->getFileName(); + } catch (\ReflectionException) { + return false; + } + } + + static public function tryLoadType(string $autoloader, string $type): void { + $loader = static::loadLoader($autoloader); + + // try to load class via loader, otherwise assume that loader script + // did some magic and loaded class via other ways + $phpFilePath = $loader->findFile($type); + if ($phpFilePath && !TwigUtils::include($phpFilePath)) { + throw new \Exception("Class $type is found but cannot be loaded"); + } + } + + + static public function getNamespaceCompletions(string $autoloader, string $namespace): array { + $loader = static::loadLoader($autoloader); + + echo "COMPL: $namespace" . PHP_EOL; + + $prefixesPsr4ToPaths = $loader->getPrefixesPsr4(); + $prefixesPsr4 = array_keys($prefixesPsr4ToPaths); + + if ($namespace === '\\') { + return $prefixesPsr4; + } + + /** @var array */ + $classesInNamespace = []; + // $namespaceFirstPart = '\\' . strtok($namespace, '\\'); + $namespaceFirstPart = strtok($namespace, '\\'); + + foreach ($prefixesPsr4 as $prefix) { + if (!str_starts_with($prefix, $namespaceFirstPart)) { + continue; + } + + $dir = $prefixesPsr4ToPaths[$prefix][0]; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); + + foreach ($iterator as $file) { + if ($file->isDir()) continue; + if (pathinfo($file->getFilename(), PATHINFO_EXTENSION) === 'php') { + try { + // TODO(zekfad): need something better than blindly + // executing all php scripts in the project + // this footgun can demolish the face of caller + // I remember having some rm -rf random_folder + // in my scripts + // Probably worse reflection or something like that + // can help analyze code without it's execution + // TwigUtils::include_once($file->getPathname()); + } catch (\Throwable) {} + } + } + + foreach (get_declared_classes() as $class) { + if (!str_starts_with($class, $namespace)) { + continue; + } + + $classesInNamespace[$class] = true; + } + } + return $classesInNamespace; + } + + static public function getTypeCompletions(string $type): array { + if (str_contains($type, '&') || str_contains($type, '(') || str_contains($type, ')')) { + throw new \Exception("Complex types definitions such as intersection type are not yet supported"); + } + + $completions = []; + + $tok = strtok($type, '|'); + while ($tok !== false) { + if (!in_array(strtolower($tok), static::PRIMITIVE_TYPES)) { + $completions = array_merge($completions, static::getClassCompletion($type)); + } + $tok = strtok('|'); + } + strtok('', ''); + + return $completions; + } + + protected const GETTER_PREFIX = 'get'; + + protected static function getPropertyName(string $getterName): string { + return lcfirst( + substr($getterName, strlen(static::GETTER_PREFIX)), + ); + } + + protected static function typeToString(?\ReflectionType $type): string { + if (null === $type) { + return ''; + } + + if ($type instanceof \ReflectionNamedType) { + return $type->getName(); + } + + if ($type instanceof \ReflectionUnionType) { + return implode('|', array_map( + fn(\ReflectionNamedType $type) => $type->getName(), + $type->getTypes(), + )); + } + + if ($type instanceof \ReflectionIntersectionType) { + return implode('&', array_map( + fn(\ReflectionNamedType $type) => $type->getName(), + $type->getTypes(), + )); + } + + throw new \Exception('Unknown reflection type ' . gettype($type)); + } + + + static protected function getClassCompletion(string $type): array { + $refClass = new \ReflectionClass($type); + + $properties = $refClass->getProperties(\ReflectionProperty::IS_PUBLIC); + $methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC); + + $completionProperties = []; + $completionMethods = []; + + foreach ($properties as $property) { + $completionProperties[] = [ + 'name' => $property->getName(), + 'type' => static::typeToString($property->getType()), + ]; + } + + foreach ($methods as $method) { + if ($method->isConstructor() || $method->isDestructor()) { + continue; + } + + $methodName = $method->getName(); + // hide magic methods + if (str_starts_with($methodName, '__')) { + continue; + } + + $parameters = $method->getParameters(); + + if (str_starts_with($methodName, static::GETTER_PREFIX) && count($parameters) === 0) { + $completionProperties[] = [ + 'name' => static::getPropertyName($methodName), + 'type' => static::typeToString($method->getReturnType()), + ]; + } + + $completionMethods[] = [ + 'name' => $methodName, + 'type' => static::typeToString($method->getReturnType()), + 'parameters' => array_map( + fn(\ReflectionParameter $parameter) => [ + 'name' => $parameter->getName(), + 'type' => static::typeToString($parameter->getType()), + 'isOptional' => $parameter->isOptional(), + 'isVariadic' => $parameter->isVariadic(), + ], + $parameters, + ), + ]; + } + + return [ + 'properties' => $completionProperties, + 'methods' => $completionMethods, + ]; + } +} diff --git a/packages/language-server/phpUtils/src/TwigUtils.php b/packages/language-server/phpUtils/src/TwigUtils.php new file mode 100644 index 0000000..5aac560 --- /dev/null +++ b/packages/language-server/phpUtils/src/TwigUtils.php @@ -0,0 +1,161 @@ +getView(); + $twig = $view->getTwig(); + if (!$twig || !($twig instanceof \Twig\Environment)) + throw new InvalidArgumentException("Craft CMS view did not return \Twig\Environment"); + + $metadata = Metadata::getTwigMetadata($twig, false); + $metadata['loader_paths'] = $view->getSiteTemplateRoots(); + + return $metadata; + case 'vanilla': + $twig = static::include($env); + if (!$twig || !($twig instanceof \Twig\Environment)) + throw new InvalidArgumentException("Environment file '$env' doesn't return \Twig\Environment or doesn't exist"); + return Metadata::getTwigMetadata($twig, true); + default: + throw new \Exception("Support for $framework is not yet implemented. Try using vanilla if applicable instead."); + } + } + + /** + * @param string[] $argv + * @return array + */ + protected static function getTypeDefinition(array $argv): array { + if (count($argv) < 1) + throw new ArgumentCountError("Missing required argument: autoloader"); + if (count($argv) < 2) + throw new ArgumentCountError("Missing required argument: type"); + + $loader_file = $argv[0]; + $type = $argv[1]; + + return [ + 'path' => Reflection::findType($loader_file, $type) ?: null, + ]; + } + + /** + * @param string[] $argv + * @return array + */ + protected static function getTypeCompletions(array $argv): array { + if (count($argv) < 1) + throw new ArgumentCountError("Missing required argument: autoloader"); + if (count($argv) < 2) + throw new ArgumentCountError("Missing required argument: type"); + + $loader_file = $argv[0]; + $type = $argv[1]; + + Reflection::tryLoadType($loader_file, $type); + try { + return Reflection::getTypeCompletions($type); + } catch (\ReflectionException $e) { + // TODO(zekfad): possibly rewrite error for a better UX? + throw $e; + } + } + + /** + * @param string[] $argv + * @return array + */ + protected static function getNamespaceCompletions(array $argv): array { + if (count($argv) < 1) + throw new ArgumentCountError("Missing required argument: autoloader"); + if (count($argv) < 2) + throw new ArgumentCountError("Missing required argument: namespace"); + + $loader_file = $argv[0]; + $namespace = $argv[1]; + + return Reflection::getNamespaceCompletions($loader_file, $namespace); + } +} diff --git a/packages/language-server/src/configuration/ConfigurationManager.ts b/packages/language-server/src/configuration/ConfigurationManager.ts index 01c9cc5..b539ed7 100644 --- a/packages/language-server/src/configuration/ConfigurationManager.ts +++ b/packages/language-server/src/configuration/ConfigurationManager.ts @@ -34,6 +34,7 @@ export class ConfigurationManager { autoInsertSpaces: true, inlayHints: InlayHintProvider.defaultSettings, phpExecutable: 'php', + autoloaderPath: 'vendor/autoload.php', symfonyConsolePath: './bin/console', vanillaTwigEnvironmentPath: '', framework: PhpFrameworkOption.Symfony, @@ -82,7 +83,11 @@ export class ConfigurationManager { console.info('Guessed `twiggy.framework`: ', config.framework); } - const phpExecutor = new PhpExecutor(config.phpExecutable, workspaceDirectory); + const phpExecutor = new PhpExecutor( + config.phpExecutable, + config.autoloaderPath, + workspaceDirectory, + ); const twigCodeStyleFixer = config.diagnostics.twigCsFixer ? new TwigCodeStyleFixer(phpExecutor, workspaceDirectory) : null; diff --git a/packages/language-server/src/configuration/LanguageServerSettings.ts b/packages/language-server/src/configuration/LanguageServerSettings.ts index ffebfeb..2eaccba 100644 --- a/packages/language-server/src/configuration/LanguageServerSettings.ts +++ b/packages/language-server/src/configuration/LanguageServerSettings.ts @@ -23,6 +23,7 @@ export type LanguageServerSettings = { framework?: PhpFrameworkOption, phpExecutable: string, + autoloaderPath: string, symfonyConsolePath: string, vanillaTwigEnvironmentPath: string, diagnostics: DiagnosticsSettings, diff --git a/packages/language-server/src/definitions/index.ts b/packages/language-server/src/definitions/index.ts index 5d8a98b..5d9a6e6 100644 --- a/packages/language-server/src/definitions/index.ts +++ b/packages/language-server/src/definitions/index.ts @@ -10,7 +10,7 @@ import { Document, DocumentCache } from '../documents'; import { getStringNodeValue } from '../utils/node'; import { pointToPosition } from '../utils/position'; import { positionsEqual } from '../utils/position/comparePositions'; -import { documentUriToFsPath } from '../utils/uri'; +import { documentUriToFsPath, toDocumentUri } from '../utils/uri'; import { PhpExecutor } from '../phpInterop/PhpExecutor'; import { findParentByType } from '../utils/node/findParentByType'; import { SyntaxNode } from 'web-tree-sitter'; @@ -138,7 +138,7 @@ export class DefinitionProvider { if (!result?.path) return; return { - uri: result.path, + uri: toDocumentUri(result.path), range: getNodeRange(typeIdentifierNode), }; } diff --git a/packages/language-server/src/phpInterop/IPhpExecutor.ts b/packages/language-server/src/phpInterop/IPhpExecutor.ts index a350755..346e54a 100644 --- a/packages/language-server/src/phpInterop/IPhpExecutor.ts +++ b/packages/language-server/src/phpInterop/IPhpExecutor.ts @@ -1,4 +1,5 @@ import { ReflectedType } from './ReflectedType'; +import { TwigEnvironment } from 'twigEnvironment/types'; export interface IPhpExecutor { call(command: string, args: string[]): Promise<{ @@ -7,7 +8,10 @@ export interface IPhpExecutor { } | null>; callJson(command: string, args: string[]): Promise; + getEnvironment(environmentPath: string, framework: string): Promise; getClassDefinition(className: string): Promise<{ path: string | null; } | null>; + /** @deprecated TODO(zekfad): rename to getNamespaceCompletions */ getClassCompletion(className: string): Promise; + /** @deprecated TODO(zekfad): rename to getTypeCompletions */ reflectType(className: string): Promise; } diff --git a/packages/language-server/src/phpInterop/PhpExecutor.ts b/packages/language-server/src/phpInterop/PhpExecutor.ts index 2ad9b81..b7c5f39 100644 --- a/packages/language-server/src/phpInterop/PhpExecutor.ts +++ b/packages/language-server/src/phpInterop/PhpExecutor.ts @@ -1,19 +1,29 @@ +import { parseDebugTwigOutput, SymfonyTwigDebugJsonOutput } from 'twigEnvironment/symfony/parseDebugTwigOutput'; import { PhpUtilPath } from '../twigEnvironment/PhpUtilPath'; -import { exec } from '../utils/exec'; +import { CommandResult, exec } from '../utils/exec'; import { IPhpExecutor } from './IPhpExecutor'; import { ReflectedType } from './ReflectedType'; +import { TwigEnvironment } from 'twigEnvironment/types'; + + +type UtilsPharResult = { error: true, message: string, } | { error: false, result: T, }; export class PhpExecutor implements IPhpExecutor { constructor( private readonly _phpExecutable: string | undefined, + private readonly _autoloaderPath: string | undefined, private readonly _workspaceDirectory: string, ) { if (!this._phpExecutable) { console.warn('`twiggy.phpExecutable` is not configured. Some features will be disabled.'); } + + if (!this._autoloaderPath) { + console.warn('`twiggy.autoloaderPath` is not configured. Some features will be disabled.'); + } } - async call(command: string, args: string[]) { + async call(command: string, args: string[]): Promise { if (!this._phpExecutable) { return null; } @@ -25,9 +35,10 @@ export class PhpExecutor implements IPhpExecutor { cwd: this._workspaceDirectory }); - if (result.stderr) { + if (result.stderr && command !== PhpUtilPath.utilsPhar) { + // log errors for non utils phar console.error( - `Command "${command} ${args.join(' ')}" failed with following message:`, + `Command "${command} ${args.join(' ')}" produced following error message:`, result.stderr, ); @@ -51,24 +62,81 @@ export class PhpExecutor implements IPhpExecutor { return JSON.parse(result.stdout) as TResult; } + private async callUtilsPhar(...args: string[]): Promise { + const callResult = await this.call(PhpUtilPath.utilsPhar, args); + if (!callResult) { + throw new Error('Failed to execute utils phar'); + } + + if (callResult.stderr) { + // log stderr for utils phar as warning + console.warn('Utils phar warning:', callResult.stderr); + } + + const result = JSON.parse(callResult.stdout) as UtilsPharResult + if (result.error) { + throw new Error(`Utils phar returned an error: ${result.message}`); + } + + return result.result; + } + + async getEnvironment(framework: string, environmentPath: string): Promise { + return parseDebugTwigOutput(await this.callUtilsPhar( + 'get-env', + framework, + environmentPath, + )); + } + + async reflectType(className: string): Promise { + if (!this._autoloaderPath) { + return null; + } + + try { + return await this.callUtilsPhar( + 'get-type-completions', + this._autoloaderPath, + className, + ); + } catch (error) { + console.debug(`Failed to reflect type ${className}`, error); + return null; + } + } + async getClassDefinition(className: string) { - return await this.callJson<{ path: string | null }>(PhpUtilPath.getDefinitionPhp, [ - this._workspaceDirectory, - `'${className}'`, - ]); + if (!this._autoloaderPath) { + return null; + } + + try { + return await this.callUtilsPhar<{ path: string | null }>( + 'get-type-definition', + this._autoloaderPath, + className, + ); + } catch (error) { + console.debug(`Failed to get type definition ${className}`, error); + return null; + } } async getClassCompletion(className: string) { - return await this.callJson(PhpUtilPath.getCompletionPhp, [ - this._workspaceDirectory, - `'${className}'`, - ]) || []; - } + if (!this._autoloaderPath) { + return []; + } - async reflectType(className: string) { - return await this.callJson(PhpUtilPath.reflectType, [ - this._workspaceDirectory, - `'${className}'`, - ]); + try { + return await this.callUtilsPhar( + 'get-namespace-completions', + this._autoloaderPath, + className, + ); + } catch (error) { + console.debug(`Failed to get namespace completion ${className}`, error); + return []; + } } } diff --git a/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts b/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts index ae0c9e3..697235a 100644 --- a/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts +++ b/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts @@ -21,17 +21,13 @@ export class CraftTwigEnvironment implements IFrameworkTwigEnvironment { this.#environment = await this.#loadEnvironment(workspaceDirectory); } - async #loadEnvironment(workspaceDirectory: string): Promise { - const result = await this._phpExecutor.callJson( - PhpUtilPath.getCraftTwig, [ - workspaceDirectory, - ] - ); - - if (!result) { + #loadEnvironment(workspaceDirectory: string): Promise { + return this._phpExecutor.getEnvironment( + 'craft', + workspaceDirectory, + ).catch((error) => { + console.error("Failed to load vanilla twig environment:", error); return null; - } - - return parseDebugTwigOutput(result); + }); } } diff --git a/packages/language-server/src/twigEnvironment/PhpUtilPath.ts b/packages/language-server/src/twigEnvironment/PhpUtilPath.ts index c090667..7225f38 100644 --- a/packages/language-server/src/twigEnvironment/PhpUtilPath.ts +++ b/packages/language-server/src/twigEnvironment/PhpUtilPath.ts @@ -1,9 +1,5 @@ import path from 'node:path'; export const PhpUtilPath = { - getCraftTwig: path.resolve(__dirname, './phpUtils/printCraftTwigEnvironment.php'), - printTwigEnvironment: path.resolve(__dirname, './phpUtils/printTwigEnvironment.php'), - getDefinitionPhp: path.resolve(__dirname, './phpUtils/definitionClassPsr4.php'), - getCompletionPhp: path.resolve(__dirname, './phpUtils/completeClassPsr4.php'), - reflectType: path.resolve(__dirname, './phpUtils/reflectType.php'), + utilsPhar: path.resolve(__dirname, './phpUtils/twiggy-php-utils.phar'), } as const; diff --git a/packages/language-server/src/twigEnvironment/SymfonyTwigEnvironment.ts b/packages/language-server/src/twigEnvironment/SymfonyTwigEnvironment.ts index f7332ab..f524257 100644 --- a/packages/language-server/src/twigEnvironment/SymfonyTwigEnvironment.ts +++ b/packages/language-server/src/twigEnvironment/SymfonyTwigEnvironment.ts @@ -1,4 +1,4 @@ -import { PhpExecutor } from '../phpInterop/PhpExecutor'; +import { IPhpExecutor } from 'phpInterop/IPhpExecutor'; import { isFile } from '../utils/files/fileStat'; import { EmptyEnvironment, IFrameworkTwigEnvironment } from './IFrameworkTwigEnvironment'; import { TwigEnvironmentArgs } from './TwigEnvironmentArgs'; @@ -11,7 +11,7 @@ export class SymfonyTwigEnvironment implements IFrameworkTwigEnvironment { #symfonyConsolePath: string | undefined; - constructor(private readonly _phpExecutor: PhpExecutor) { + constructor(private readonly _phpExecutor: IPhpExecutor) { } get environment() { diff --git a/packages/language-server/src/twigEnvironment/VanillaTwigEnvironment.ts b/packages/language-server/src/twigEnvironment/VanillaTwigEnvironment.ts index 98d3a84..dc4f20e 100644 --- a/packages/language-server/src/twigEnvironment/VanillaTwigEnvironment.ts +++ b/packages/language-server/src/twigEnvironment/VanillaTwigEnvironment.ts @@ -1,15 +1,13 @@ -import { PhpExecutor } from '../phpInterop/PhpExecutor'; +import { IPhpExecutor } from 'phpInterop/IPhpExecutor'; import { EmptyEnvironment, IFrameworkTwigEnvironment } from './IFrameworkTwigEnvironment'; -import { PhpUtilPath } from './PhpUtilPath'; import { TwigEnvironmentArgs } from './TwigEnvironmentArgs'; -import { SymfonyTwigDebugJsonOutput, parseDebugTwigOutput } from './symfony/parseDebugTwigOutput'; import { RouteNameToPathRecord, TemplatePathMapping, TwigEnvironment } from './types'; export class VanillaTwigEnvironment implements IFrameworkTwigEnvironment { #environment: TwigEnvironment | null = null; #routes: RouteNameToPathRecord = {}; - constructor(private readonly _phpExecutor: PhpExecutor) { + constructor(private readonly _phpExecutor: IPhpExecutor) { } get environment() { @@ -30,17 +28,13 @@ export class VanillaTwigEnvironment implements IFrameworkTwigEnvironment { this.#environment = await this.#loadEnvironment(vanillaTwigEnvironmentPath); } - async #loadEnvironment(vanillaTwigEnvironmentPath: string): Promise { - const result = await this._phpExecutor.callJson( - PhpUtilPath.printTwigEnvironment, [ - vanillaTwigEnvironmentPath, - ] - ); - - if (!result) { + #loadEnvironment(vanillaTwigEnvironmentPath: string): Promise { + return this._phpExecutor.getEnvironment( + 'vanilla', + vanillaTwigEnvironmentPath, + ).catch((error) => { + console.error("Failed to load vanilla twig environment:", error); return null; - } - - return parseDebugTwigOutput(result); + }); } } diff --git a/packages/language-server/src/utils/node/parseFunctionCall.ts b/packages/language-server/src/utils/node/parseFunctionCall.ts index 5d91528..d96da4c 100644 --- a/packages/language-server/src/utils/node/parseFunctionCall.ts +++ b/packages/language-server/src/utils/node/parseFunctionCall.ts @@ -31,12 +31,15 @@ export const parseFunctionCall = ( ? nameNode : nameNode.childForFieldName('property'); + if (!functionNameNode) + return void 0; + const argNodes = node.childForFieldName('arguments')?.namedChildren; const args = argNodes?.map(toFunctionCallArgument) || []; return { object: objectNode?.text, - name: functionNameNode!.text, + name: functionNameNode.text, args, }; }; diff --git a/packages/vscode/build/index.mjs b/packages/vscode/build/index.mjs index cab351f..59fb982 100755 --- a/packages/vscode/build/index.mjs +++ b/packages/vscode/build/index.mjs @@ -74,7 +74,8 @@ const buildOptions = /** @type {const} @satisfies {esbuild.BuildOptions} */ ({ assets: [ { watch: isDev, from: '../language-server/node_modules/web-tree-sitter/tree-sitter.wasm', to: './dist/' }, { watch: isDev, from: grammarWasmPath, to: './dist/' }, - { watch: isDev, from: '../language-server/phpUtils/**/*', to: './dist/phpUtils/' }, + { watch: isDev, from: '../language-server/phpUtils/twiggy-php-utils.phar', to: './dist/phpUtils/' }, + // { watch: isDev, from: '../language-server/phpUtils/**/*', to: './dist/phpUtils/' }, ], }), ], diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 585b854..5465d9d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -66,6 +66,12 @@ "default": "php", "markdownDescription": "Points to the PHP executable." }, + "twiggy.autoloaderPath": { + "type": "string", + "scope": "resource", + "default": "vendor/autoload.php", + "markdownDescription": "Points to the autoload.php file." + }, "twiggy.framework": { "type": "string", "scope": "resource", From 2d32b427ed55031df78b2dd41fd1deba904c6f58 Mon Sep 17 00:00:00 2001 From: Mikhail Gunin Date: Thu, 9 Jan 2025 10:33:27 +0300 Subject: [PATCH 2/4] chore: add postinstall --- package.json | 1 + packages/language-server/phpUtils/.gitignore | 1 - packages/language-server/phpUtils/src/TwigUtils.php | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2b3d70d..a5b195b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "private": true, "scripts": { + "postinstall": "cd packages/language-server/phpUtils && composer install", "build": "pnpm run --dir ./packages/vscode build", "dev": "pnpm run --dir ./packages/vscode dev", "build-grammar-wasm": "pnpm run --dir ./packages/tree-sitter-twig build-wasm" diff --git a/packages/language-server/phpUtils/.gitignore b/packages/language-server/phpUtils/.gitignore index 1239218..a318208 100644 --- a/packages/language-server/phpUtils/.gitignore +++ b/packages/language-server/phpUtils/.gitignore @@ -1,3 +1,2 @@ vendor/ -# remove compiled artefact from version control twiggy-php-utils.phar diff --git a/packages/language-server/phpUtils/src/TwigUtils.php b/packages/language-server/phpUtils/src/TwigUtils.php index 5aac560..50d1082 100644 --- a/packages/language-server/phpUtils/src/TwigUtils.php +++ b/packages/language-server/phpUtils/src/TwigUtils.php @@ -67,7 +67,7 @@ protected static function getTwigEnvironment(array $argv): array { $framework = $argv[0]; $env = $argv[1]; - + $twig = null; switch ($framework) { case 'craft': @@ -104,7 +104,7 @@ protected static function getTwigEnvironment(array $argv): array { } /** - * @param string[] $argv + * @param string[] $argv * @return array */ protected static function getTypeDefinition(array $argv): array { @@ -122,7 +122,7 @@ protected static function getTypeDefinition(array $argv): array { } /** - * @param string[] $argv + * @param string[] $argv * @return array */ protected static function getTypeCompletions(array $argv): array { @@ -144,7 +144,7 @@ protected static function getTypeCompletions(array $argv): array { } /** - * @param string[] $argv + * @param string[] $argv * @return array */ protected static function getNamespaceCompletions(array $argv): array { From 4abef4e02ca06b162a0b84af3e06ed35c4f978c3 Mon Sep 17 00:00:00 2001 From: Mikhail Gunin Date: Thu, 9 Jan 2025 10:34:12 +0300 Subject: [PATCH 3/4] refac: simplify PharUtilPath --- packages/language-server/src/phpInterop/PhpExecutor.ts | 6 +++--- .../src/twigEnvironment/CraftTwigEnvironment.ts | 2 -- .../language-server/src/twigEnvironment/PharUtilPath.ts | 3 +++ packages/language-server/src/twigEnvironment/PhpUtilPath.ts | 5 ----- 4 files changed, 6 insertions(+), 10 deletions(-) create mode 100644 packages/language-server/src/twigEnvironment/PharUtilPath.ts delete mode 100644 packages/language-server/src/twigEnvironment/PhpUtilPath.ts diff --git a/packages/language-server/src/phpInterop/PhpExecutor.ts b/packages/language-server/src/phpInterop/PhpExecutor.ts index b7c5f39..8eae11e 100644 --- a/packages/language-server/src/phpInterop/PhpExecutor.ts +++ b/packages/language-server/src/phpInterop/PhpExecutor.ts @@ -1,5 +1,5 @@ import { parseDebugTwigOutput, SymfonyTwigDebugJsonOutput } from 'twigEnvironment/symfony/parseDebugTwigOutput'; -import { PhpUtilPath } from '../twigEnvironment/PhpUtilPath'; +import { PharUtilPath } from '../twigEnvironment/PharUtilPath'; import { CommandResult, exec } from '../utils/exec'; import { IPhpExecutor } from './IPhpExecutor'; import { ReflectedType } from './ReflectedType'; @@ -35,7 +35,7 @@ export class PhpExecutor implements IPhpExecutor { cwd: this._workspaceDirectory }); - if (result.stderr && command !== PhpUtilPath.utilsPhar) { + if (result.stderr && command !== PharUtilPath) { // log errors for non utils phar console.error( `Command "${command} ${args.join(' ')}" produced following error message:`, @@ -63,7 +63,7 @@ export class PhpExecutor implements IPhpExecutor { } private async callUtilsPhar(...args: string[]): Promise { - const callResult = await this.call(PhpUtilPath.utilsPhar, args); + const callResult = await this.call(PharUtilPath, args); if (!callResult) { throw new Error('Failed to execute utils phar'); } diff --git a/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts b/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts index 697235a..afce7b4 100644 --- a/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts +++ b/packages/language-server/src/twigEnvironment/CraftTwigEnvironment.ts @@ -1,8 +1,6 @@ import { PhpExecutor } from '../phpInterop/PhpExecutor'; import { EmptyEnvironment, IFrameworkTwigEnvironment } from './IFrameworkTwigEnvironment'; -import { PhpUtilPath } from './PhpUtilPath'; import { TwigEnvironmentArgs } from './TwigEnvironmentArgs'; -import { SymfonyTwigDebugJsonOutput, parseDebugTwigOutput } from './symfony/parseDebugTwigOutput'; import { TwigEnvironment } from './types'; export class CraftTwigEnvironment implements IFrameworkTwigEnvironment { diff --git a/packages/language-server/src/twigEnvironment/PharUtilPath.ts b/packages/language-server/src/twigEnvironment/PharUtilPath.ts new file mode 100644 index 0000000..a9e0ed5 --- /dev/null +++ b/packages/language-server/src/twigEnvironment/PharUtilPath.ts @@ -0,0 +1,3 @@ +import path from 'node:path'; + +export const PharUtilPath = path.resolve(__dirname, './phpUtils/twiggy-php-utils.phar'); diff --git a/packages/language-server/src/twigEnvironment/PhpUtilPath.ts b/packages/language-server/src/twigEnvironment/PhpUtilPath.ts deleted file mode 100644 index 7225f38..0000000 --- a/packages/language-server/src/twigEnvironment/PhpUtilPath.ts +++ /dev/null @@ -1,5 +0,0 @@ -import path from 'node:path'; - -export const PhpUtilPath = { - utilsPhar: path.resolve(__dirname, './phpUtils/twiggy-php-utils.phar'), -} as const; From bf09ceca9062255c0fbbad31cdb439d12e0f7ed8 Mon Sep 17 00:00:00 2001 From: Mikhail Gunin Date: Thu, 9 Jan 2025 11:40:23 +0300 Subject: [PATCH 4/4] chore: rebuild phar on php change --- packages/language-server/phpUtils/composer.json | 2 +- packages/vscode/build/index.mjs | 11 ++++++++++- packages/vscode/package.json | 1 + pnpm-lock.yaml | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/language-server/phpUtils/composer.json b/packages/language-server/phpUtils/composer.json index 8f77a99..836d1dd 100644 --- a/packages/language-server/phpUtils/composer.json +++ b/packages/language-server/phpUtils/composer.json @@ -21,6 +21,6 @@ "twig/twig": "4.x-dev" }, "scripts": { - "build": "vendor/bin/phar-composer build ." + "build": "rm -f twiggy-php-utils.phar && vendor/bin/phar-composer build ." } } diff --git a/packages/vscode/build/index.mjs b/packages/vscode/build/index.mjs index 59fb982..2305d5f 100755 --- a/packages/vscode/build/index.mjs +++ b/packages/vscode/build/index.mjs @@ -2,6 +2,7 @@ import { execSync } from 'node:child_process'; import * as esbuild from 'esbuild'; +import * as chokidar from 'chokidar'; import { cpSync as cp, rmSync as rm, existsSync } from 'node:fs'; import copyPlugin from 'esbuild-plugin-copy'; @@ -75,7 +76,6 @@ const buildOptions = /** @type {const} @satisfies {esbuild.BuildOptions} */ ({ { watch: isDev, from: '../language-server/node_modules/web-tree-sitter/tree-sitter.wasm', to: './dist/' }, { watch: isDev, from: grammarWasmPath, to: './dist/' }, { watch: isDev, from: '../language-server/phpUtils/twiggy-php-utils.phar', to: './dist/phpUtils/' }, - // { watch: isDev, from: '../language-server/phpUtils/**/*', to: './dist/phpUtils/' }, ], }), ], @@ -95,6 +95,15 @@ async function main() { const ctx = await esbuild.context(buildOptions); if (isDev) { + const phpWatcher = chokidar.watch([ + '../language-server/phpUtils/index.php', + '../language-server/phpUtils/src/', + ]); + + phpWatcher.on('change', () => { + execSync('composer build', { cwd: '../language-server/phpUtils', stdio: 'inherit' }); + }); + await ctx.watch(); return; } diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 5465d9d..cbc5694 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -160,6 +160,7 @@ "devDependencies": { "@types/node": "^20.12.7", "@types/vscode": "^1.88.0", + "chokidar": "^4.0.3", "esbuild": "^0.20.2", "esbuild-plugin-copy": "^2.1.1", "typescript": "^5.4.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b780865..8bc498b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,9 @@ importers: '@types/vscode': specifier: ^1.88.0 version: 1.88.0 + chokidar: + specifier: ^4.0.3 + version: 4.0.3 esbuild: specifier: ^0.20.2 version: 0.20.2 @@ -286,6 +289,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -460,6 +467,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -726,6 +737,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.0.2 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -915,6 +930,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + resolve-pkg-maps@1.0.0: {} reusify@1.0.4: {}