mirror of
https://github.com/danog/psalm-plugin.git
synced 2024-11-26 12:25:05 +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.cache
|
||||
|
||||
# phpunit cache
|
||||
.phpunit.result.cache
|
||||
|
||||
# test logs
|
||||
/tests/logs/*
|
||||
|
@ -5,7 +5,6 @@ return PhpCsFixer\Config::create()
|
||||
\Symfony\Component\Finder\Finder::create()
|
||||
->in([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
])
|
||||
)
|
||||
->setRiskyAllowed(true)
|
||||
|
@ -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"/>
|
||||
|
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/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.
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
<plugins>
|
||||
<pluginClass class="Psl\Integration\Psalm\Plugin" />
|
||||
<pluginClass class="Psl\Psalm\Plugin" />
|
||||
</plugins>
|
||||
</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