mirror of
https://github.com/danog/psalm-plugin.git
synced 2024-11-29 20:29:02 +01:00
create psalm plugin
This commit is contained in:
parent
cd223caa18
commit
af20829f44
36
.github/workflows/code-coverage.yml
vendored
36
.github/workflows/code-coverage.yml
vendored
@ -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
|
|
27
.github/workflows/security-analysis.yml
vendored
27
.github/workflows/security-analysis.yml
vendored
@ -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"
|
|
48
.github/workflows/unit-tests.yml
vendored
48
.github/workflows/unit-tests.yml
vendored
@ -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
6
.gitignore
vendored
@ -15,9 +15,3 @@
|
|||||||
|
|
||||||
# php-cs-fixer cache
|
# php-cs-fixer cache
|
||||||
.php_cs.cache
|
.php_cs.cache
|
||||||
|
|
||||||
# phpunit cache
|
|
||||||
.phpunit.result.cache
|
|
||||||
|
|
||||||
# test logs
|
|
||||||
/tests/logs/*
|
|
||||||
|
@ -5,7 +5,6 @@ return PhpCsFixer\Config::create()
|
|||||||
\Symfony\Component\Finder\Finder::create()
|
\Symfony\Component\Finder\Finder::create()
|
||||||
->in([
|
->in([
|
||||||
__DIR__ . '/src',
|
__DIR__ . '/src',
|
||||||
__DIR__ . '/tests',
|
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
->setRiskyAllowed(true)
|
->setRiskyAllowed(true)
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
<description>The coding standard for PHP Standard Library.</description>
|
<description>The coding standard for PHP Standard Library.</description>
|
||||||
|
|
||||||
<file>src</file>
|
<file>src</file>
|
||||||
<file>tests</file>
|
|
||||||
|
|
||||||
<arg name="basepath" value="."/>
|
<arg name="basepath" value="."/>
|
||||||
<arg name="colors"/>
|
<arg name="colors"/>
|
||||||
|
57
README.md
57
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/psalm-plugin/workflows/static%20analysis/badge.svg)
|
||||||
![Static analysis status](https://github.com/php-standard-library/repository-template/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)
|
||||||
![Security analysis status](https://github.com/php-standard-library/repository-template/workflows/security%20analysis/badge.svg)
|
[![Total Downloads](https://poser.pugx.org/php-standard-library/psalm-plugin/d/total.svg)](https://packagist.org/packages/php-standard-library/psalm-plugin)
|
||||||
![Coding standards status](https://github.com/php-standard-library/repository-template/workflows/coding%20standards/badge.svg)
|
[![Latest Stable Version](https://poser.pugx.org/php-standard-library/psalm-plugin/v/stable.svg)](https://packagist.org/packages/php-standard-library/psalm-plugin)
|
||||||
[![Type Coverage](https://shepherd.dev/github/php-standard-library/repository-template/coverage.svg)](https://shepherd.dev/github/php-standard-library/repository-template)
|
[![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
|
## Sponsors
|
||||||
|
|
||||||
Thanks to our sponsors and supporters:
|
Thanks to our sponsors and supporters:
|
||||||
|
|
||||||
|
|
||||||
| JetBrains |
|
| 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> |
|
| <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
|
## License
|
||||||
|
|
||||||
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
|
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "php-standard-library/repository-template",
|
"name": "php-standard-library/psalm-plugin",
|
||||||
"description": "PHP Standard Library: Repository Template",
|
"description": "Psalm plugin for the PHP Standard Library",
|
||||||
"type": "library",
|
"type": "psalm-plugin",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
@ -12,27 +12,16 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^7.4 || ^8.0",
|
"php": "^7.4 || ^8.0",
|
||||||
"azjezz/psl": "^1.5",
|
"azjezz/psl": "^1.5",
|
||||||
"ext-bcmath": "*",
|
"vimeo/psalm": "^4.6"
|
||||||
"ext-json": "*",
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"ext-sodium": "*",
|
|
||||||
"ext-intl": "*"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^9.5",
|
|
||||||
"friendsofphp/php-cs-fixer": "^2.18",
|
"friendsofphp/php-cs-fixer": "^2.18",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"squizlabs/php_codesniffer": "^3.5",
|
"squizlabs/php_codesniffer": "^3.5"
|
||||||
"vimeo/psalm": "dev-master"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Psl\\": "src/"
|
"Psl\\Psalm\\": "src/"
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload-dev": {
|
|
||||||
"psr-4": {
|
|
||||||
"Psl\\Tests\\": "tests/"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -46,18 +35,21 @@
|
|||||||
],
|
],
|
||||||
"type:check": "psalm",
|
"type:check": "psalm",
|
||||||
"type:coverage": "psalm --shepherd",
|
"type:coverage": "psalm --shepherd",
|
||||||
"test:unit": "phpunit",
|
|
||||||
"code:coverage": "php-coveralls -v",
|
"code:coverage": "php-coveralls -v",
|
||||||
"security:analysis": "psalm --taint-analysis",
|
"security:analysis": "psalm --taint-analysis",
|
||||||
"check": [
|
"check": [
|
||||||
"@cs:check",
|
"@cs:check",
|
||||||
"@type:check",
|
"@type:check",
|
||||||
"@security:analysis",
|
"@security:analysis"
|
||||||
"@test:unit"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"process-timeout": 1200,
|
"process-timeout": 1200,
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"psalm": {
|
||||||
|
"pluginClass": "Psl\\Psalm\\Plugin"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -43,6 +43,6 @@
|
|||||||
</issueHandlers>
|
</issueHandlers>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
<pluginClass class="Psl\Integration\Psalm\Plugin" />
|
<pluginClass class="Psl\Psalm\Plugin" />
|
||||||
</plugins>
|
</plugins>
|
||||||
</psalm>
|
</psalm>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
84
src/EventHandler/Type/Shape/FunctionReturnTypeProvider.php
Normal file
84
src/EventHandler/Type/Shape/FunctionReturnTypeProvider.php
Normal 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
21
src/Plugin.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user