mirror of
https://github.com/danog/psalm-plugin-symfony.git
synced 2024-11-26 20:04:58 +01:00
[console] params analyze considering PHP8 named arguments (#183)
This commit is contained in:
parent
f2a2aee2c8
commit
ce6bb2995b
18
.github/workflows/integrate.yaml
vendored
18
.github/workflows/integrate.yaml
vendored
@ -131,17 +131,9 @@ jobs:
|
|||||||
- name: "Build acceptance tests with codeception"
|
- name: "Build acceptance tests with codeception"
|
||||||
run: vendor/bin/codecept build
|
run: vendor/bin/codecept build
|
||||||
|
|
||||||
- name: "Run base acceptance tests with codeception"
|
- name: "Run acceptance tests with codeception"
|
||||||
run: vendor/bin/codecept run -v -g symfony-common
|
run: vendor/bin/codecept run -v -g symfony-common -g symfony-${{ matrix.symfony-version }}
|
||||||
|
|
||||||
- name: "Run Symfony 3 acceptance tests with codeception"
|
- name: "Run acceptance tests with codeception PHP8 only tests"
|
||||||
if: matrix.symfony-version == '3'
|
run: vendor/bin/codecept run -v -g php-8
|
||||||
run: vendor/bin/codecept run -v -g symfony-3
|
if: matrix.php-version == '8.0'
|
||||||
|
|
||||||
- name: "Run symfony 4 acceptance tests with codeception"
|
|
||||||
if: matrix.symfony-version == '4'
|
|
||||||
run: vendor/bin/codecept run -v -g symfony-4
|
|
||||||
|
|
||||||
- name: "Run Symfony 5 acceptance tests with codeception"
|
|
||||||
if: matrix.symfony-version == '5'
|
|
||||||
run: vendor/bin/codecept run -v -g symfony-5
|
|
||||||
|
@ -25,6 +25,7 @@ use Psalm\Type\Atomic\TString;
|
|||||||
use Psalm\Type\Union;
|
use Psalm\Type\Union;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
|
|
||||||
class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
||||||
{
|
{
|
||||||
@ -116,17 +117,20 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
|||||||
*/
|
*/
|
||||||
private static function analyseArgument(array $args, StatementsSource $statements_source): void
|
private static function analyseArgument(array $args, StatementsSource $statements_source): void
|
||||||
{
|
{
|
||||||
$identifier = self::getNodeIdentifier($args[0]->value);
|
$normalizedParams = self::normalizeArgumentParams($args);
|
||||||
|
|
||||||
|
$identifier = self::getNodeIdentifier($normalizedParams['name']->value);
|
||||||
if (!$identifier) {
|
if (!$identifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($args) > 1) {
|
$modeParam = $normalizedParams['mode'];
|
||||||
|
if ($modeParam) {
|
||||||
try {
|
try {
|
||||||
$mode = self::getModeValue($args[1]->value);
|
$mode = self::getModeValue($modeParam->value);
|
||||||
} catch (InvalidConsoleModeException $e) {
|
} catch (InvalidConsoleModeException $e) {
|
||||||
IssueBuffer::accepts(
|
IssueBuffer::accepts(
|
||||||
new InvalidConsoleArgumentValue(new CodeLocation($statements_source, $args[1]->value)),
|
new InvalidConsoleArgumentValue(new CodeLocation($statements_source, $modeParam->value)),
|
||||||
$statements_source->getSuppressedIssues()
|
$statements_source->getSuppressedIssues()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -144,10 +148,10 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
|||||||
$returnTypes = new Union([new TString(), new TNull()]);
|
$returnTypes = new Union([new TString(), new TNull()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($args[3])) {
|
$defaultParam = $normalizedParams['default'];
|
||||||
$defaultArg = $args[3];
|
if ($defaultParam) {
|
||||||
$returnTypes->removeType('null');
|
$returnTypes->removeType('null');
|
||||||
if ($defaultArg->value instanceof Expr\ConstFetch && 'null' === $defaultArg->value->name->parts[0]) {
|
if ($defaultParam->value instanceof Expr\ConstFetch && 'null' === $defaultParam->value->name->parts[0]) {
|
||||||
$returnTypes->addType(new TNull());
|
$returnTypes->addType(new TNull());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,7 +164,9 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
|||||||
*/
|
*/
|
||||||
private static function analyseOption(array $args, StatementsSource $statements_source): void
|
private static function analyseOption(array $args, StatementsSource $statements_source): void
|
||||||
{
|
{
|
||||||
$identifier = self::getNodeIdentifier($args[0]->value);
|
$normalizedParams = self::normalizeOptionParams($args);
|
||||||
|
|
||||||
|
$identifier = self::getNodeIdentifier($normalizedParams['name']->value);
|
||||||
if (!$identifier) {
|
if (!$identifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -169,12 +175,13 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
|||||||
$identifier = substr($identifier, 2);
|
$identifier = substr($identifier, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($args[2])) {
|
$modeOption = $normalizedParams['mode'];
|
||||||
|
if ($modeOption) {
|
||||||
try {
|
try {
|
||||||
$mode = self::getModeValue($args[2]->value);
|
$mode = self::getModeValue($modeOption->value);
|
||||||
} catch (InvalidConsoleModeException $e) {
|
} catch (InvalidConsoleModeException $e) {
|
||||||
IssueBuffer::accepts(
|
IssueBuffer::accepts(
|
||||||
new InvalidConsoleOptionValue(new CodeLocation($statements_source, $args[2]->value)),
|
new InvalidConsoleOptionValue(new CodeLocation($statements_source, $modeOption->value)),
|
||||||
$statements_source->getSuppressedIssues()
|
$statements_source->getSuppressedIssues()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -186,11 +193,11 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
|||||||
|
|
||||||
$returnTypes = new Union([new TString(), new TNull()]);
|
$returnTypes = new Union([new TString(), new TNull()]);
|
||||||
|
|
||||||
if (isset($args[4])) {
|
$defaultParam = $normalizedParams['default'];
|
||||||
$defaultArg = $args[4];
|
if ($defaultParam) {
|
||||||
$returnTypes->removeType('null');
|
$returnTypes->removeType('null');
|
||||||
if ($defaultArg->value instanceof Expr\ConstFetch) {
|
if ($defaultParam->value instanceof Expr\ConstFetch) {
|
||||||
switch ($defaultArg->value->name->parts[0]) {
|
switch ($defaultParam->value->name->parts[0]) {
|
||||||
case 'null':
|
case 'null':
|
||||||
$returnTypes->addType(new TNull());
|
$returnTypes->addType(new TNull());
|
||||||
break;
|
break;
|
||||||
@ -217,6 +224,46 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface
|
|||||||
self::$options[$identifier] = $returnTypes;
|
self::$options[$identifier] = $returnTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Arg> $args
|
||||||
|
*
|
||||||
|
* @psalm-return array{name: Arg, shortcut: ?Arg, mode: ?Arg, description: ?Arg, default: ?Arg}
|
||||||
|
*/
|
||||||
|
private static function normalizeOptionParams(array $args): array
|
||||||
|
{
|
||||||
|
return self::normalizeParams(['name', 'shortcut', 'mode', 'description', 'default'], $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Arg> $args
|
||||||
|
*
|
||||||
|
* @psalm-return array{name: Arg, mode: ?Arg, description: ?Arg, default: ?Arg}
|
||||||
|
*/
|
||||||
|
private static function normalizeArgumentParams(array $args): array
|
||||||
|
{
|
||||||
|
return self::normalizeParams(['name', 'mode', 'description', 'default'], $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeParams(array $params, array $args): array
|
||||||
|
{
|
||||||
|
$result = array_fill_keys($params, null);
|
||||||
|
foreach ($args as $arg) {
|
||||||
|
if ($arg->name) {
|
||||||
|
$name = $arg->name->name;
|
||||||
|
|
||||||
|
$key = array_search($name, $params);
|
||||||
|
Assert::integer($key);
|
||||||
|
$params = array_slice($params, $key + 1);
|
||||||
|
} else {
|
||||||
|
$name = array_shift($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$name] = $arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $mode
|
* @param mixed $mode
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
@symfony-common @php-8
|
||||||
|
Feature: ConsoleArgument named arguments with PHP8
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I have Symfony plugin enabled
|
||||||
|
And I have the following code preamble
|
||||||
|
"""
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Assert adding console argument skipping default arguments with named arguments works as expected
|
||||||
|
Given I have the following code
|
||||||
|
"""
|
||||||
|
class MyCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->addArgument('test', default: 'test');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
/** @psalm-trace $argument */
|
||||||
|
$argument = $input->getArgument('test');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
When I run Psalm
|
||||||
|
Then I see these errors
|
||||||
|
| Type | Message |
|
||||||
|
| Trace | $argument: string |
|
||||||
|
And I see no other errors
|
@ -0,0 +1,39 @@
|
|||||||
|
@symfony-common @php-8
|
||||||
|
Feature: ConsoleOption named arguments with PHP8
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I have Symfony plugin enabled
|
||||||
|
And I have the following code preamble
|
||||||
|
"""
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Assert adding options skipping default arguments with named arguments works as expected
|
||||||
|
Given I have the following code
|
||||||
|
"""
|
||||||
|
class MyCommand extends Command
|
||||||
|
{
|
||||||
|
public function configure(): void
|
||||||
|
{
|
||||||
|
$this->addOption('test', mode: InputOption::VALUE_REQUIRED, default: 'test');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
/** @psalm-trace $string */
|
||||||
|
$string = $input->getOption('test');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
When I run Psalm
|
||||||
|
Then I see these errors
|
||||||
|
| Type | Message |
|
||||||
|
| Trace | $string: string |
|
||||||
|
And I see no other errors
|
Loading…
Reference in New Issue
Block a user