From af20829f447569c56b65822237c4481572d274e4 Mon Sep 17 00:00:00 2001 From: azjezz Date: Fri, 26 Mar 2021 11:05:12 +0100 Subject: [PATCH] create psalm plugin --- .github/workflows/code-coverage.yml | 36 -------- .github/workflows/security-analysis.yml | 27 ------ .github/workflows/unit-tests.yml | 48 ----------- .gitignore | 6 -- .php_cs.dist | 1 - .phpcs.xml | 1 - README.md | 57 +++++++++++-- composer.json | 34 +++----- phpunit.xml.dist | 21 ----- psalm.xml | 2 +- src/.gitkeep | 0 .../Optional/FunctionReturnTypeProvider.php | 45 ++++++++++ .../Type/Shape/FunctionReturnTypeProvider.php | 84 +++++++++++++++++++ src/Plugin.php | 21 +++++ tests/.gitkeep | 0 15 files changed, 213 insertions(+), 170 deletions(-) delete mode 100644 .github/workflows/code-coverage.yml delete mode 100644 .github/workflows/security-analysis.yml delete mode 100644 .github/workflows/unit-tests.yml delete mode 100644 phpunit.xml.dist delete mode 100644 src/.gitkeep create mode 100644 src/EventHandler/Type/Optional/FunctionReturnTypeProvider.php create mode 100644 src/EventHandler/Type/Shape/FunctionReturnTypeProvider.php create mode 100644 src/Plugin.php delete mode 100644 tests/.gitkeep diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml deleted file mode 100644 index 4a692a2..0000000 --- a/.github/workflows/code-coverage.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "code coverage" - -on: - pull_request: ~ - push: ~ - -jobs: - code-coverage: - name: "code coverage" - - runs-on: "ubuntu-latest" - - steps: - - name: "checkout" - uses: "actions/checkout@v2" - - - name: "installing PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "8.0" - ini-values: memory_limit=-1 - tools: composer:v2, cs2pr - extensions: bcmath, mbstring, intl, sodium, json - - - name: "installing dependencies" - run: "composer install --no-interaction --no-progress --ignore-platform-req php" - - - name: "running unit tests ( phpunit )" - run: "php vendor/bin/phpunit" - - - name: "sending code coverage to coveralls" - env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - composer global require php-coveralls/php-coveralls - php-coveralls -x tests/logs/clover.xml -o tests/logs/coveralls-upload.json -v diff --git a/.github/workflows/security-analysis.yml b/.github/workflows/security-analysis.yml deleted file mode 100644 index 6f716fd..0000000 --- a/.github/workflows/security-analysis.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: "security analysis" - -on: - pull_request: ~ - push: ~ - -jobs: - security-analysis: - name: "security analysis" - runs-on: "ubuntu-latest" - steps: - - name: "checkout" - uses: "actions/checkout@v2" - - - name: "installing PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "7.4" - ini-values: memory_limit=-1 - tools: composer:v2, cs2pr - extensions: bcmath, mbstring, intl, sodium, json - - - name: "installing dependencies" - run: "composer update --no-interaction --no-progress" - - - name: "running security analysis ( psalm )" - run: "vendor/bin/psalm --output-format=github --taint-analysis" diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml deleted file mode 100644 index d75d4c6..0000000 --- a/.github/workflows/unit-tests.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: "unit tests" - -on: - pull_request: ~ - push: ~ - -jobs: - unit-tests: - name: "unit tests" - - runs-on: ${{ matrix.operating-system }} - - strategy: - matrix: - php-version: - - "7.4" - - "8.0" - operating-system: - - "macos-latest" - - "ubuntu-latest" - - "windows-latest" - - steps: - - name: "checkout" - uses: "actions/checkout@v2" - - - name: "installing PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "${{ matrix.php-version }}" - ini-values: memory_limit=-1 - tools: composer:v2, cs2pr - extensions: bcmath, mbstring, intl, sodium, json - - - name: "caching dependencies" - uses: "actions/cache@v2" - with: - path: | - ~/.composer/cache - vendor - key: "php-${{ matrix.php-version }}" - restore-keys: "php-${{ matrix.php-version }}" - - - name: "installing dependencies" - run: "composer install --no-interaction --no-progress --ignore-platform-req php" - - - name: "running unit tests ( phpunit )" - run: "php vendor/bin/phpunit" diff --git a/.gitignore b/.gitignore index 7b5b5cb..08857f0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,3 @@ # php-cs-fixer cache .php_cs.cache - -# phpunit cache -.phpunit.result.cache - -# test logs -/tests/logs/* diff --git a/.php_cs.dist b/.php_cs.dist index a29aa1c..d024000 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -5,7 +5,6 @@ return PhpCsFixer\Config::create() \Symfony\Component\Finder\Finder::create() ->in([ __DIR__ . '/src', - __DIR__ . '/tests', ]) ) ->setRiskyAllowed(true) diff --git a/.phpcs.xml b/.phpcs.xml index 9ac1ae8..fe67f46 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -4,7 +4,6 @@ The coding standard for PHP Standard Library. src - tests diff --git a/README.md b/README.md index eef5e01..c130b49 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,62 @@ -# Repository Template +# PSL Psalm Plugin -![Unit tests status](https://github.com/php-standard-library/repository-template/workflows/unit%20tests/badge.svg) -![Static analysis status](https://github.com/php-standard-library/repository-template/workflows/static%20analysis/badge.svg) -![Security analysis status](https://github.com/php-standard-library/repository-template/workflows/security%20analysis/badge.svg) -![Coding standards status](https://github.com/php-standard-library/repository-template/workflows/coding%20standards/badge.svg) -[![Type Coverage](https://shepherd.dev/github/php-standard-library/repository-template/coverage.svg)](https://shepherd.dev/github/php-standard-library/repository-template) +![Static analysis status](https://github.com/php-standard-library/psalm-plugin/workflows/static%20analysis/badge.svg) +[![Type Coverage](https://shepherd.dev/github/php-standard-library/psalm-plugin/coverage.svg)](https://shepherd.dev/github/php-standard-library/psalm-plugin) +[![Total Downloads](https://poser.pugx.org/php-standard-library/psalm-plugin/d/total.svg)](https://packagist.org/packages/php-standard-library/psalm-plugin) +[![Latest Stable Version](https://poser.pugx.org/php-standard-library/psalm-plugin/v/stable.svg)](https://packagist.org/packages/php-standard-library/psalm-plugin) +[![License](https://poser.pugx.org/php-standard-library/psalm-plugin/license.svg)](https://packagist.org/packages/php-standard-library/psalm-plugin) + +## Installation + +Supported installation method is via [composer](https://getcomposer.org): + +```shell +composer install php-standard-library/psalm-plugin --dev +``` + +## Usage + +To enable the plugin, add the `Psl\Psalm\Plugin` class to your psalm configuration using `psalm-plugin` binary as follows: + +```shell +php vendor/bin/psalm-plugin enable php-standard-library/psalm-plugin +``` + +## Type improvements + +Given the following example: + +```php +use Psl\Type; + +$specification = Type\shape([ + 'name' => Type\string(), + 'age' => Type\int(), + 'location' => Type\optional(Type\shape([ + 'city' => Type\string(), + 'state' => Type\string(), + 'country' => Type\string(), + ])) +]); + +$input = $specification->coerce($_GET['user']); + +/** @psalm-trace $input */ +``` + +Psalm assumes that `$input` is of type `array<"age"|"location"|"name", array<"city"|"country"|"state", string>|int|string>`. + +If we enable the `php-standard-library/psalm-plugin` plugin, you will get a more specific +and correct type of `array{name: string, age: int, location?: array{city: string, state: string, country: string}}`. ## Sponsors Thanks to our sponsors and supporters: - | JetBrains | |---| | | - ## License The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. diff --git a/composer.json b/composer.json index cf1bca5..0fbf20b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { - "name": "php-standard-library/repository-template", - "description": "PHP Standard Library: Repository Template", - "type": "library", + "name": "php-standard-library/psalm-plugin", + "description": "Psalm plugin for the PHP Standard Library", + "type": "psalm-plugin", "license": "MIT", "authors": [ { @@ -12,27 +12,16 @@ "require": { "php": "^7.4 || ^8.0", "azjezz/psl": "^1.5", - "ext-bcmath": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-sodium": "*", - "ext-intl": "*" + "vimeo/psalm": "^4.6" }, "require-dev": { - "phpunit/phpunit": "^9.5", "friendsofphp/php-cs-fixer": "^2.18", "roave/security-advisories": "dev-master", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "dev-master" + "squizlabs/php_codesniffer": "^3.5" }, "autoload": { "psr-4": { - "Psl\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Psl\\Tests\\": "tests/" + "Psl\\Psalm\\": "src/" } }, "scripts": { @@ -46,18 +35,21 @@ ], "type:check": "psalm", "type:coverage": "psalm --shepherd", - "test:unit": "phpunit", "code:coverage": "php-coveralls -v", "security:analysis": "psalm --taint-analysis", "check": [ "@cs:check", "@type:check", - "@security:analysis", - "@test:unit" + "@security:analysis" ] }, "config": { "process-timeout": 1200, "sort-packages": true + }, + "extra": { + "psalm": { + "pluginClass": "Psl\\Psalm\\Plugin" + } } -} \ No newline at end of file +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 4c6ef43..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - src - - - - - - - - - - - tests/ - - - - diff --git a/psalm.xml b/psalm.xml index b946715..b30f500 100644 --- a/psalm.xml +++ b/psalm.xml @@ -43,6 +43,6 @@ - + diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/EventHandler/Type/Optional/FunctionReturnTypeProvider.php b/src/EventHandler/Type/Optional/FunctionReturnTypeProvider.php new file mode 100644 index 0000000..7177fd1 --- /dev/null +++ b/src/EventHandler/Type/Optional/FunctionReturnTypeProvider.php @@ -0,0 +1,45 @@ + + */ + public static function getFunctionIds(): array + { + return [ + 'psl\type\optional' + ]; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union + { + $argument = Iter\first($event->getCallArgs()); + if (null === $argument) { + return null; + } + + $type = $event + ->getStatementsSource() + ->getNodeTypeProvider() + ->getType($argument->value); + + if (null === $type) { + return null; + } + + $clone = clone $type; + $clone->possibly_undefined = true; + + return $clone; + } +} diff --git a/src/EventHandler/Type/Shape/FunctionReturnTypeProvider.php b/src/EventHandler/Type/Shape/FunctionReturnTypeProvider.php new file mode 100644 index 0000000..9b78851 --- /dev/null +++ b/src/EventHandler/Type/Shape/FunctionReturnTypeProvider.php @@ -0,0 +1,84 @@ + + */ + public static function getFunctionIds(): array + { + return [ + 'psl\type\shape' + ]; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union + { + $argument = Iter\first($event->getCallArgs()); + if (null === $argument) { + return new Type\Union([new Type\Atomic\TGenericObject(TypeInterface::class, [ + new Type\Union([ + new Type\Atomic\TArray([ + new Type\Union([new Type\Atomic\TArrayKey()]), + new Type\Union([new Type\Atomic\TMixed()]) + ]) + ]) + ])]); + } + + $statements_source = $event->getStatementsSource(); + $type = $statements_source->getNodeTypeProvider()->getType($argument->value); + if (null === $type) { + return new Type\Union([new Type\Atomic\TGenericObject(TypeInterface::class, [ + new Type\Union([ + new Type\Atomic\TArray([ + new Type\Union([new Type\Atomic\TArrayKey()]), + new Type\Union([new Type\Atomic\TMixed()]) + ]) + ]) + ])]); + } + + $atomic = $type->getAtomicTypes(); + $argument_shape = $atomic['array'] ?? null; + if (!$argument_shape instanceof Type\Atomic\TKeyedArray) { + return new Type\Union([new Type\Atomic\TGenericObject(TypeInterface::class, [ + new Type\Union([ + new Type\Atomic\TArray([ + new Type\Union([new Type\Atomic\TArrayKey()]), + new Type\Union([new Type\Atomic\TMixed()]) + ]) + ]) + ])]); + } + + $properties = []; + foreach ($argument_shape->properties as $name => $value) { + $type = Iter\first($value->getAtomicTypes()); + if (!$type instanceof Type\Atomic\TGenericObject) { + return null; + } + + $property_type = clone $type->type_params[0]; + $property_type->possibly_undefined = $value->possibly_undefined; + + $properties[$name] = $property_type; + } + + return new Type\Union([new Type\Atomic\TGenericObject(TypeInterface::class, [ + new Type\Union([ + new Type\Atomic\TKeyedArray($properties) + ]) + ])]); + } +} diff --git a/src/Plugin.php b/src/Plugin.php new file mode 100644 index 0000000..e470905 --- /dev/null +++ b/src/Plugin.php @@ -0,0 +1,21 @@ +registerHooksFromClass(EventHandler\Type\Optional\FunctionReturnTypeProvider::class); + $registration->registerHooksFromClass(EventHandler\Type\Shape\FunctionReturnTypeProvider::class); + } +} diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000