[console] params analyze considering PHP8 named arguments (#183)

This commit is contained in:
Farhad Safarov 2021-06-04 12:00:11 +03:00 committed by GitHub
parent f2a2aee2c8
commit ce6bb2995b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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