create psalm plugin

This commit is contained in:
azjezz 2021-03-26 11:05:12 +01:00
parent cd223caa18
commit af20829f44
15 changed files with 213 additions and 170 deletions

View File

@ -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

View File

@ -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"

View File

@ -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"

6
.gitignore vendored
View File

@ -15,9 +15,3 @@
# php-cs-fixer cache
.php_cs.cache
# phpunit cache
.phpunit.result.cache
# test logs
/tests/logs/*

View File

@ -5,7 +5,6 @@ return PhpCsFixer\Config::create()
\Symfony\Component\Finder\Finder::create()
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
)
->setRiskyAllowed(true)

View File

@ -4,7 +4,6 @@
<description>The coding standard for PHP Standard Library.</description>
<file>src</file>
<file>tests</file>
<arg name="basepath" value="."/>
<arg name="colors"/>

View File

@ -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 |
|---|
| <a href="https://www.jetbrains.com/?from=PSL ( PHP Standard Library )" title="JetBrains" target="_blank"><img src="https://res.cloudinary.com/azjezz/image/upload/v1599239910/jetbrains_qnyb0o.png" height="120" /></a> |
## License
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.

View File

@ -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"
}
}
}

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" colors="true" stopOnFailure="true" bootstrap="vendor/autoload.php">
<coverage processUncoveredFiles="true">
<include>
<directory>src</directory>
</include>
<report>
<clover outputFile="tests/logs/clover.xml" />
</report>
</coverage>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="PHP Standard Library">
<directory>tests/</directory>
</testsuite>
</testsuites>
<logging />
</phpunit>

View File

@ -43,6 +43,6 @@
</issueHandlers>
<plugins>
<pluginClass class="Psl\Integration\Psalm\Plugin" />
<pluginClass class="Psl\Psalm\Plugin" />
</plugins>
</psalm>

View File

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Psl\Psalm\EventHandler\Type\Optional;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psl\Iter;
final class FunctionReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
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;
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Psl\Psalm\EventHandler\Type\Shape;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psl\Iter;
use Psl\Type\TypeInterface;
final class FunctionReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
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)
])
])]);
}
}

21
src/Plugin.php Normal file
View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Psl\Psalm;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use SimpleXMLElement;
final class Plugin implements PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
{
require_once __DIR__ . '/EventHandler/Type/Optional/FunctionReturnTypeProvider.php';
require_once __DIR__ . '/EventHandler/Type/Shape/FunctionReturnTypeProvider.php';
$registration->registerHooksFromClass(EventHandler\Type\Optional\FunctionReturnTypeProvider::class);
$registration->registerHooksFromClass(EventHandler\Type\Shape\FunctionReturnTypeProvider::class);
}
}

View File