diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml
index 620a5f93..b0f637a1 100644
--- a/.github/workflows/integrate.yaml
+++ b/.github/workflows/integrate.yaml
@@ -6,7 +6,7 @@ on:
branches:
- master
schedule:
- - cron: "0 10 * * *"
+ - cron: "0 10 * * 6"
jobs:
static-code-analysis:
@@ -18,8 +18,8 @@ jobs:
fail-fast: false
matrix:
php-version:
- - 8.0
- 8.1
+ - 8.2
dependencies:
- highest
@@ -27,7 +27,7 @@ jobs:
steps:
- name: "Checkout"
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
@@ -36,12 +36,12 @@ jobs:
php-version: ${{ matrix.php-version }}
- name: Install Composer dependencies
- uses: ramsey/composer-install@v1
+ uses: ramsey/composer-install@v3
with:
dependency-versions: ${{ matrix.dependencies }}
- name: "Cache cache directory for vimeo/psalm"
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: .build/psalm
key: php-${{ matrix.php-version }}-psalm-${{ github.sha }}
@@ -64,26 +64,23 @@ jobs:
fail-fast: false
matrix:
php-version:
- - 8.0
- 8.1
- 8.2
symfony-version:
- - 5
- 6
- - 7
+ - 7.0
+ - 7.1
exclude:
- - php-version: 8.0
- symfony-version: 6
- - php-version: 8.0
- symfony-version: 7
- php-version: 8.1
- symfony-version: 7
+ symfony-version: 7.0
+ - php-version: 8.1
+ symfony-version: 7.1
steps:
- name: "Checkout"
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
@@ -96,7 +93,7 @@ jobs:
run: composer config extra.symfony.require ${{ matrix.symfony-version }}.*
- name: Install Composer dependencies
- uses: ramsey/composer-install@v1
+ uses: ramsey/composer-install@v3
- name: "Run unit tests with phpunit"
run: vendor/bin/phpunit --configuration=phpunit.xml
diff --git a/README.md b/README.md
index 753e59c2..ccac6260 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ vendor/bin/psalm-plugin enable psalm/plugin-symfony
| Symfony Psalm Plugin | PHP | Symfony | Psalm |
|----------------------|------------|---------|-------|
-| 5.x | ^7.4, ^8.0 | 5, 6 | 5 |
+| 5.x | ^8.0 | 5, 6, 7 | 5 |
| 4.x | ^7.4, ^8.0 | 4, 5, 6 | 4 |
| 3.x | ^7.1, ^8.0 | 4, 5, 6 | 4 |
| 2.x | ^7.1, ^8.0 | 4, 5 | 4 |
@@ -131,7 +131,7 @@ To leverage the real Twig file analyzer, you have to configure a checker for the
```xml
-
+
```
diff --git a/composer.json b/composer.json
index 6d3bc610..9b77764b 100644
--- a/composer.json
+++ b/composer.json
@@ -10,20 +10,20 @@
}
],
"require": {
- "php": "^7.4 || ^8.0",
+ "php": "^8.1",
"ext-simplexml": "*",
"symfony/framework-bundle": "^5.0 || ^6.0 || ^7.0",
- "vimeo/psalm": "^5.1"
+ "vimeo/psalm": "^5.24"
},
"require-dev": {
- "symfony/form": "^5.0 || ^6.0 || ^7.0",
"doctrine/annotations": "^1.8|^2",
"doctrine/orm": "^2.9",
"phpunit/phpunit": "~7.5 || ~9.5",
"symfony/cache-contracts": "^1.0 || ^2.0",
"symfony/console": "*",
+ "symfony/form": "^5.0 || ^6.0 || ^7.0",
"symfony/messenger": "^5.0 || ^6.0 || ^7.0",
- "symfony/security-guard": "*",
+ "symfony/security-core": "*",
"symfony/serializer": "^5.0 || ^6.0 || ^7.0",
"symfony/validator": "*",
"twig/twig": "^2.10 || ^3.0",
diff --git a/src/Handler/ConsoleHandler.php b/src/Handler/ConsoleHandler.php
index c18f511f..def0894f 100644
--- a/src/Handler/ConsoleHandler.php
+++ b/src/Handler/ConsoleHandler.php
@@ -57,7 +57,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
break;
case 'Symfony\Component\Console\Input\InputInterface::getargument':
$identifier = self::getNodeIdentifier($args[0]->value);
- if (!$identifier) {
+ if (null === $identifier) {
break;
}
@@ -70,7 +70,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
break;
case 'Symfony\Component\Console\Input\InputInterface::getoption':
$identifier = self::getNodeIdentifier($args[0]->value);
- if (!$identifier) {
+ if (null === $identifier) {
break;
}
@@ -126,7 +126,7 @@ private static function analyseArgument(array $args, StatementsSource $statement
$normalizedParams = self::normalizeArgumentParams($args);
$identifier = self::getNodeIdentifier($normalizedParams['name']->value);
- if (!$identifier) {
+ if (null === $identifier) {
return;
}
@@ -170,7 +170,7 @@ private static function analyseOption(array $args, StatementsSource $statements_
$normalizedParams = self::normalizeOptionParams($args);
$identifier = self::getNodeIdentifier($normalizedParams['name']->value);
- if (!$identifier) {
+ if (null === $identifier) {
return;
}
diff --git a/src/Handler/ContainerHandler.php b/src/Handler/ContainerHandler.php
index c22ae57f..e750debc 100644
--- a/src/Handler/ContainerHandler.php
+++ b/src/Handler/ContainerHandler.php
@@ -35,10 +35,7 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi
'Symfony\Bundle\FrameworkBundle\Test\TestContainer',
];
- /**
- * @var ContainerMeta|null
- */
- private static $containerMeta;
+ private static ?ContainerMeta $containerMeta = null;
/**
* @var array collection of cower-cased class names that are present in the container
@@ -105,7 +102,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
if ('self' === $className) {
$className = $event->getStatementsSource()->getSource()->getFQCLN();
}
- if (!$idArgument->name instanceof Identifier || !$className) {
+ if (!$idArgument->name instanceof Identifier || null === $className) {
return;
}
@@ -114,7 +111,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
} else {
try {
$serviceId = \constant($className.'::'.$idArgument->name->name);
- } catch (\Exception $e) {
+ } catch (\Exception) {
return;
}
}
@@ -133,7 +130,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
}
$class = $service->getClass();
- if ($class) {
+ if (null !== $class) {
$codebase->classlikes->addFullyQualifiedClassName($class);
$event->setReturnTypeCandidate(new Union([new TNamedObject($class)]));
}
@@ -141,7 +138,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
if (!$service->isPublic()) {
/** @var class-string $kernelTestCaseClass */
$kernelTestCaseClass = 'Symfony\Bundle\FrameworkBundle\Test\KernelTestCase';
- $isTestContainer = $context->parent
+ $isTestContainer = null !== $context->parent
&& ($kernelTestCaseClass === $context->parent
|| is_subclass_of($context->parent, $kernelTestCaseClass)
);
@@ -152,7 +149,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
);
}
}
- } catch (ServiceNotFoundException $e) {
+ } catch (ServiceNotFoundException) {
IssueBuffer::accepts(
new ServiceNotFound($serviceId, new CodeLocation($statements_source, $firstArg->value)),
$statements_source->getSuppressedIssues()
@@ -160,7 +157,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
}
}
- public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
+ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void
{
$codebase = $event->getCodebase();
$statements_source = $event->getStatementsSource();
@@ -221,7 +218,7 @@ function ($c) use ($methodName) {
private static function followsParameterNamingConvention(string $name): bool
{
- if (0 === strpos($name, 'env(')) {
+ if (str_starts_with($name, 'env(')) {
return true;
}
diff --git a/src/Handler/HeaderBagHandler.php b/src/Handler/HeaderBagHandler.php
index e5827089..401a17ad 100644
--- a/src/Handler/HeaderBagHandler.php
+++ b/src/Handler/HeaderBagHandler.php
@@ -6,7 +6,6 @@
use PhpParser\Node\Scalar\String_;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
-use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TNull;
@@ -24,7 +23,7 @@ public static function getClassLikeNames(): array
];
}
- public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Type\Union
+ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{
$fq_classlike_name = $event->getFqClasslikeName();
$method_name_lowercase = $event->getMethodNameLowercase();
diff --git a/src/Handler/ParameterBagHandler.php b/src/Handler/ParameterBagHandler.php
index f8a84ad1..46c01d16 100644
--- a/src/Handler/ParameterBagHandler.php
+++ b/src/Handler/ParameterBagHandler.php
@@ -42,30 +42,18 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
}
$argument = $expr->args[0]->value->value;
+
try {
- $parameter = self::$containerMeta->getParameter($argument);
- } catch (ParameterNotFoundException $e) {
+ $parameterTypes = self::$containerMeta->guessParameterType($argument);
+ } catch (ParameterNotFoundException) {
// maybe emit ParameterNotFound issue
return;
}
- // @todo find a better way to calculate return type
- switch (gettype($parameter)) {
- case 'string':
- $event->setReturnTypeCandidate(new Union([Atomic::create('string')]));
- break;
- case 'boolean':
- $event->setReturnTypeCandidate(new Union([Atomic::create('bool')]));
- break;
- case 'integer':
- $event->setReturnTypeCandidate(new Union([Atomic::create('int')]));
- break;
- case 'double':
- $event->setReturnTypeCandidate(new Union([Atomic::create('float')]));
- break;
- case 'array':
- $event->setReturnTypeCandidate(new Union([Atomic::create('array')]));
- break;
+ if (null === $parameterTypes || [] === $parameterTypes) {
+ return;
}
+
+ $event->setReturnTypeCandidate(new Union(array_map(fn (string $parameterType): Atomic => Atomic::create($parameterType), $parameterTypes)));
}
}
diff --git a/src/Plugin.php b/src/Plugin.php
index ab61d402..ce9c5431 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -29,7 +29,7 @@
*/
class Plugin implements PluginEntryPointInterface
{
- public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config = null): void
+ public function __invoke(RegistrationInterface $registration, ?\SimpleXMLElement $config = null): void
{
require_once __DIR__.'/Handler/HeaderBagHandler.php';
require_once __DIR__.'/Handler/ContainerHandler.php';
@@ -39,13 +39,13 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config =
require_once __DIR__.'/Handler/DoctrineQueryBuilderHandler.php';
require_once __DIR__.'/Provider/FormGetErrorsReturnTypeProvider.php';
- $api->registerHooksFromClass(HeaderBagHandler::class);
- $api->registerHooksFromClass(ConsoleHandler::class);
- $api->registerHooksFromClass(ContainerDependencyHandler::class);
- $api->registerHooksFromClass(RequiredSetterHandler::class);
+ $registration->registerHooksFromClass(HeaderBagHandler::class);
+ $registration->registerHooksFromClass(ConsoleHandler::class);
+ $registration->registerHooksFromClass(ContainerDependencyHandler::class);
+ $registration->registerHooksFromClass(RequiredSetterHandler::class);
if (class_exists(\Doctrine\ORM\QueryBuilder::class)) {
- $api->registerHooksFromClass(DoctrineQueryBuilderHandler::class);
+ $registration->registerHooksFromClass(DoctrineQueryBuilderHandler::class);
}
if (class_exists(AnnotationRegistry::class)) {
@@ -54,10 +54,10 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config =
/** @psalm-suppress DeprecatedMethod */
AnnotationRegistry::registerLoader('class_exists');
}
- $api->registerHooksFromClass(DoctrineRepositoryHandler::class);
+ $registration->registerHooksFromClass(DoctrineRepositoryHandler::class);
require_once __DIR__.'/Handler/AnnotationHandler.php';
- $api->registerHooksFromClass(AnnotationHandler::class);
+ $registration->registerHooksFromClass(AnnotationHandler::class);
}
if (isset($config->containerXml)) {
@@ -83,14 +83,14 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config =
require_once __DIR__.'/Handler/ParameterBagHandler.php';
ParameterBagHandler::init($containerMeta);
- $api->registerHooksFromClass(ParameterBagHandler::class);
+ $registration->registerHooksFromClass(ParameterBagHandler::class);
}
- $api->registerHooksFromClass(ContainerHandler::class);
+ $registration->registerHooksFromClass(ContainerHandler::class);
- $this->addStubs($api, __DIR__.'/Stubs/common');
- $this->addStubs($api, __DIR__.'/Stubs/'.Kernel::MAJOR_VERSION);
- $this->addStubs($api, __DIR__.'/Stubs/php');
+ $this->addStubs($registration, __DIR__.'/Stubs/common');
+ $this->addStubs($registration, __DIR__.'/Stubs/'.Kernel::MAJOR_VERSION);
+ $this->addStubs($registration, __DIR__.'/Stubs/php');
if (isset($config->twigCachePath)) {
$twig_cache_path = getcwd().DIRECTORY_SEPARATOR.ltrim((string) $config->twigCachePath, DIRECTORY_SEPARATOR);
@@ -99,15 +99,15 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config =
}
require_once __DIR__.'/Twig/CachedTemplatesTainter.php';
- $api->registerHooksFromClass(CachedTemplatesTainter::class);
+ $registration->registerHooksFromClass(CachedTemplatesTainter::class);
require_once __DIR__.'/Twig/CachedTemplatesMapping.php';
- $api->registerHooksFromClass(CachedTemplatesMapping::class);
+ $registration->registerHooksFromClass(CachedTemplatesMapping::class);
CachedTemplatesMapping::setCachePath($twig_cache_path);
}
require_once __DIR__.'/Twig/AnalyzedTemplatesTainter.php';
- $api->registerHooksFromClass(AnalyzedTemplatesTainter::class);
+ $registration->registerHooksFromClass(AnalyzedTemplatesTainter::class);
if (isset($config->twigRootPath)) {
$twig_root_path = trim((string) $config->twigRootPath, DIRECTORY_SEPARATOR);
@@ -119,20 +119,20 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config =
TemplateFileAnalyzer::setTemplateRootPath($twig_root_path);
}
- $api->registerHooksFromClass(FormGetErrorsReturnTypeProvider::class);
+ $registration->registerHooksFromClass(FormGetErrorsReturnTypeProvider::class);
}
- private function addStubs(RegistrationInterface $api, string $path): void
+ private function addStubs(RegistrationInterface $registration, string $path): void
{
if (!is_dir($path)) {
- // e.g. looking for stubs for version 3, but there aren't any at time of writing, so don't try and load them.
+ // e.g., looking for stubs for version 3, but there aren't any at time of writing, so don't try and load them.
return;
}
$a = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
foreach ($a as $file) {
if (!$file->isDir()) {
- $api->addStubFile($file->getPathname());
+ $registration->addStubFile($file->getPathname());
}
}
}
diff --git a/src/Stubs/common/Component/PropertyAccess/PropertyAccessorInterface.stubphp b/src/Stubs/common/Component/PropertyAccess/PropertyAccessorInterface.stubphp
index db8bd358..607809e5 100644
--- a/src/Stubs/common/Component/PropertyAccess/PropertyAccessorInterface.stubphp
+++ b/src/Stubs/common/Component/PropertyAccess/PropertyAccessorInterface.stubphp
@@ -11,7 +11,7 @@ interface PropertyAccessorInterface
* @psalm-param mixed $value
* @psalm-param-out T $objectOrArray
*/
- public function setValue(&$objectOrArray, $propertyPath, $value);
+ public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value);
/**
* @param object|array $objectOrArray
@@ -19,7 +19,7 @@ interface PropertyAccessorInterface
*
* @return mixed
*/
- public function getValue($objectOrArray, $propertyPath);
+ public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed;
/**
* @param object|array $objectOrArray
@@ -27,7 +27,7 @@ interface PropertyAccessorInterface
*
* @return bool
*/
- public function isWritable($objectOrArray, $propertyPath);
+ public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool;
/**
* @param object|array $objectOrArray
@@ -35,5 +35,5 @@ interface PropertyAccessorInterface
*
* @return bool
*/
- public function isReadable($objectOrArray, $propertyPath);
+ public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool;
}
diff --git a/src/Stubs/common/Component/Security/Core/Authorization/Voter/Voter.stubphp b/src/Stubs/common/Component/Security/Core/Authorization/Voter/Voter.stubphp
deleted file mode 100644
index 57d88416..00000000
--- a/src/Stubs/common/Component/Security/Core/Authorization/Voter/Voter.stubphp
+++ /dev/null
@@ -1,42 +0,0 @@
- $callback
* @psalm-return T
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null);
diff --git a/src/Symfony/ContainerMeta.php b/src/Symfony/ContainerMeta.php
index 7dfee1d9..c2a6c16a 100644
--- a/src/Symfony/ContainerMeta.php
+++ b/src/Symfony/ContainerMeta.php
@@ -9,7 +9,9 @@
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\EnvVarProcessor;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
+use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
@@ -19,22 +21,19 @@ class ContainerMeta
/**
* @var array
*/
- private $classNames = [];
+ private array $classNames = [];
/**
* @var array
*/
- private $classLocators = [];
+ private array $classLocators = [];
/**
* @var array>
*/
- private $serviceLocators = [];
+ private array $serviceLocators = [];
- /**
- * @var ContainerBuilder
- */
- private $container;
+ private ContainerBuilder $container;
public function __construct(array $containerXmlPaths)
{
@@ -44,9 +43,9 @@ public function __construct(array $containerXmlPaths)
/**
* @throws ServiceNotFoundException
*/
- public function get(string $id, string $contextClass = null): Definition
+ public function get(string $id, ?string $contextClass = null): Definition
{
- if ($contextClass && isset($this->classLocators[$contextClass]) && isset($this->serviceLocators[$this->classLocators[$contextClass]]) && isset($this->serviceLocators[$this->classLocators[$contextClass]][$id])) {
+ if (null !== $contextClass && isset($this->classLocators[$contextClass]) && isset($this->serviceLocators[$this->classLocators[$contextClass]]) && isset($this->serviceLocators[$this->classLocators[$contextClass]][$id])) {
$id = $this->serviceLocators[$this->classLocators[$contextClass]][$id];
try {
@@ -67,12 +66,32 @@ public function get(string $id, string $contextClass = null): Definition
return $definition;
}
+ public function getParameter(string $key): mixed
+ {
+ return $this->container->getParameter($key);
+ }
+
/**
- * @return mixed|null
+ * @throw ParameterNotFoundException
+ *
+ * @return ?array
*/
- public function getParameter(string $key)
+ public function guessParameterType(string $key): ?array
{
- return $this->container->getParameter($key);
+ $parameter = $this->getParameter($key);
+
+ if (is_string($parameter) && str_starts_with($parameter, '%env(')) {
+ return $this->envParameterType($parameter);
+ }
+
+ return match (gettype($parameter)) {
+ 'string' => ['string'],
+ 'boolean' => ['bool'],
+ 'integer' => ['int'],
+ 'double' => ['float'],
+ 'array' => ['array'],
+ default => null,
+ };
}
/**
@@ -91,34 +110,39 @@ private function init(array $containerXmlPaths): void
$containerXmlPath = null;
foreach ($containerXmlPaths as $filePath) {
$containerXmlPath = realpath((string) $filePath);
- if ($containerXmlPath) {
+ if (false !== $containerXmlPath) {
break;
}
}
- if (!$containerXmlPath) {
+ if (!is_string($containerXmlPath)) {
throw new ConfigException('Container xml file(s) not found!');
}
$xml->load($containerXmlPath);
foreach ($this->container->getDefinitions() as $definition) {
+ if ($definition->hasTag('container.service_locator')) {
+ continue;
+ }
+
$definitionFactory = $definition->getFactory();
if ($definition->hasTag('container.service_locator_context') && is_array($definitionFactory)) {
/** @var Reference $reference */
$reference = $definitionFactory[0];
$id = $definition->getTag('container.service_locator_context')[0]['id'];
- $this->classLocators[$this->container->getDefinition($id)->getClass() ?? $id] = (string) $reference;
- } elseif ($definition->hasTag('container.service_locator')) {
- continue;
- } elseif ($className = $definition->getClass()) {
+ try {
+ $this->classLocators[$this->container->getDefinition($id)->getClass() ?? $id] = (string) $reference;
+ } catch (ServiceNotFoundException) {
+ continue;
+ }
+ } elseif (null !== $className = $definition->getClass()) {
$this->classNames[] = $className;
}
}
foreach ($this->container->findTaggedServiceIds('container.service_locator') as $key => $_) {
- $definition = $this->container->getDefinition($key);
- foreach ($definition->getArgument(0) as $id => $argument) {
+ foreach ($this->container->getDefinition($key)->getArgument(0) as $id => $argument) {
if ($argument instanceof Reference) {
$this->addServiceLocator($key, $id, $argument);
} elseif ($argument instanceof ServiceClosureArgument) {
@@ -132,17 +156,20 @@ private function init(array $containerXmlPaths): void
}
}
- private function addServiceLocator(string $key, string $id, Reference $reference): void
+ /**
+ * @param array-key $id
+ */
+ private function addServiceLocator(string $key, mixed $id, Reference $reference): void
{
$this->serviceLocators[$key][$id] = (string) $reference;
try {
$definition = $this->getDefinition((string) $reference);
$className = $definition->getClass();
- if ($className) {
+ if (null !== $className) {
$this->classNames[] = $className;
}
- } catch (ServiceNotFoundException $e) {
+ } catch (ServiceNotFoundException) {
}
}
@@ -156,7 +183,7 @@ private function getDefinition(string $id): Definition
} catch (ServiceNotFoundException $serviceNotFoundException) {
try {
$alias = $this->container->getAlias($id);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
throw $serviceNotFoundException;
}
@@ -166,4 +193,17 @@ private function getDefinition(string $id): Definition
return $definition;
}
+
+ private function envParameterType(string $envParameter): ?array
+ {
+ // extract bool from %env(bool:ENV_PARAM)%, string from %env(string:ENV_PARAM)%
+ $type = preg_match('/^%env\((\w+):/', $envParameter, $matches) ? $matches[1] : null;
+
+ $envVarTypes = EnvVarProcessor::getProvidedTypes();
+ if (!isset($envVarTypes[$type])) {
+ return null;
+ }
+
+ return explode('|', $envVarTypes[$type]);
+ }
}
diff --git a/src/Twig/TemplateFileAnalyzer.php b/src/Twig/TemplateFileAnalyzer.php
index cbb3251f..76b968c4 100644
--- a/src/Twig/TemplateFileAnalyzer.php
+++ b/src/Twig/TemplateFileAnalyzer.php
@@ -18,15 +18,12 @@
*/
class TemplateFileAnalyzer extends FileAnalyzer
{
- /**
- * @var string
- */
- private static $rootPath = 'templates';
+ private static string $rootPath = 'templates';
/**
* @var list
*/
- private static $extensionClasses = [];
+ private static array $extensionClasses = [];
/**
* @param list $extensionClasses
@@ -37,8 +34,8 @@ public static function initExtensions(array $extensionClasses): void
}
public function analyze(
- PsalmContext $file_context = null,
- PsalmContext $global_context = null
+ ?PsalmContext $file_context = null,
+ ?PsalmContext $global_context = null
): void {
$codebase = $this->project_analyzer->getCodebase();
$taint = $codebase->taint_flow_graph;
diff --git a/tests/acceptance/acceptance/ContainerService.feature b/tests/acceptance/acceptance/ContainerService.feature
index eca77100..03e58f52 100644
--- a/tests/acceptance/acceptance/ContainerService.feature
+++ b/tests/acceptance/acceptance/ContainerService.feature
@@ -4,23 +4,26 @@ Feature: Container service
Background:
Given I have Symfony plugin enabled
-
- Scenario: Asserting psalm recognizes return type of service got via 'ContainerInterface::get()'
- Given I have the following code
+ And I have the following code preamble
"""
container->get(SomeService::class)->do();
+ $container->get(SomeService::class)->do();
}
}
"""
@@ -30,19 +33,11 @@ Feature: Container service
Scenario: Asserting psalm recognizes return type of service got via 'ContainerInterface::get()'.
Given I have the following code
"""
- container->get(SomeService::class)->nosuchmethod();
+ $container->get(SomeService::class)->nosuchmethod();
}
}
"""
@@ -55,18 +50,16 @@ Feature: Container service
Scenario: Container get(self::class) should not crash
Given I have the following code
"""
- container->get(self::class)->index();
+ $container->get(self::class)->index();
}
}
"""
When I run Psalm
Then I see these errors
- | Type | Message |
- | MissingReturnType | Method SomeController::index does not have a return type, expecting void |
+ | Type | Message |
+ | MixedMethodCall | Cannot determine the type of the object on the left hand side of this expression |
+ And I see no other errors
diff --git a/tests/acceptance/acceptance/ContainerXml.feature b/tests/acceptance/acceptance/ContainerXml.feature
index 8c4f8ab7..8021a8e1 100644
--- a/tests/acceptance/acceptance/ContainerXml.feature
+++ b/tests/acceptance/acceptance/ContainerXml.feature
@@ -7,19 +7,22 @@ Feature: Container XML config
"""
../../tests/acceptance/container.xml
"""
+ And I have the following code preamble
+ """
+ container->get('service_container')->has('lorem');
+ return $container->get('service_container')->has('lorem');
}
}
"""
@@ -29,15 +32,11 @@ Feature: Container XML config
Scenario: Psalm emits when service ID not found in container'
Given I have the following code
"""
- container->get('not_a_service')->has('lorem');
+ $container->get('not_a_service')->has('lorem');
}
}
"""
@@ -49,18 +48,12 @@ Feature: Container XML config
Scenario: Using service both via alias and class const
Given I have the following code
"""
- container->get('http_kernel');
- $this->container->get(HttpKernelInterface::class);
+ $container->get('http_kernel');
+ $container->get(HttpKernelInterface::class);
}
}
"""
@@ -70,15 +63,11 @@ Feature: Container XML config
Scenario: Using private service
Given I have the following code
"""
- container->get('private_service');
+ $container->get('private_service');
}
}
"""
diff --git a/tests/acceptance/acceptance/HeaderBag.feature b/tests/acceptance/acceptance/HeaderBag.feature
index 157278c0..ae1a753c 100644
--- a/tests/acceptance/acceptance/HeaderBag.feature
+++ b/tests/acceptance/acceptance/HeaderBag.feature
@@ -2,7 +2,7 @@
Feature: Header get
Background:
- Given I have issue handler "UnusedFunctionCall" suppressed
+ Given I have issue handler "UnusedVariable" suppressed
And I have Symfony plugin enabled
And I have the following code preamble
"""
@@ -11,24 +11,23 @@ Feature: Header get
use Symfony\Component\HttpFoundation\Request;
"""
- Scenario: HeaderBag get method return type should return `?string` (unless third argument is provided for < Sf4.4)
+ Scenario: HeaderBag get method return type should return `?string`
Given I have the following code
"""
class App
{
public function index(Request $request): void
{
- $string = $request->headers->get('nullable_string');
- if (!$string) {
- return;
- }
-
- trim($string);
+ /** @psalm-trace $nullableString */
+ $nullableString = $request->headers->get('nullable_string');
}
}
"""
When I run Psalm
- Then I see no errors
+ Then I see these errors
+ | Type | Message |
+ | Trace | $nullableString: null\|string |
+ And I see no other errors
Scenario: HeaderBag get method return type should return `string` if default value is provided with string
Given I have the following code
@@ -37,11 +36,13 @@ Feature: Header get
{
public function index(Request $request): void
{
+ /** @psalm-trace $string */
$string = $request->headers->get('string', 'string');
-
- trim($string);
}
}
"""
When I run Psalm
- Then I see no errors
+ Then I see these errors
+ | Type | Message |
+ | Trace | $string: string |
+ And I see no other errors
diff --git a/tests/acceptance/acceptance/NamingConventions.feature b/tests/acceptance/acceptance/NamingConventions.feature
index a376170f..928149fa 100644
--- a/tests/acceptance/acceptance/NamingConventions.feature
+++ b/tests/acceptance/acceptance/NamingConventions.feature
@@ -7,19 +7,22 @@ Feature: Naming conventions
"""
../../tests/acceptance/container.xml
"""
+ And I have the following code preamble
+ """
+ container->get('service_container')->has('lorem');
+ return $container->get('service_container')->has('lorem');
}
}
"""
@@ -29,15 +32,11 @@ Feature: Naming conventions
Scenario: Detects service naming convention violation
Given I have the following code
"""
- container->get('wronglyNamedService');
+ $container->get('wronglyNamedService');
}
}
"""
@@ -50,17 +49,11 @@ Feature: Naming conventions
Scenario: No service naming convention violation when using FQCNs
Given I have the following code
"""
- container->get(HttpKernelInterface::class);
+ $container->get(HttpKernelInterface::class);
}
}
"""
@@ -70,15 +63,11 @@ Feature: Naming conventions
Scenario: No naming convention violation for parameter
Given I have the following code
"""
- container->getParameter('kernel.cache_dir');
+ $container->getParameter('kernel.cache_dir');
}
}
"""
@@ -88,15 +77,11 @@ Feature: Naming conventions
Scenario: Detects parameter naming convention violation
Given I have the following code
"""
- container->getParameter('wronglyNamedParameter');
+ $container->getParameter('wronglyNamedParameter');
}
}
"""
@@ -109,15 +94,11 @@ Feature: Naming conventions
Scenario: No parameter naming convention violation when using environment variables
Given I have the following code
"""
- container->getParameter('env(SOME_ENV_VAR)');
+ $container->getParameter('env(SOME_ENV_VAR)');
}
}
"""
diff --git a/tests/acceptance/acceptance/PropertyAccessorInterface.feature b/tests/acceptance/acceptance/PropertyAccessorInterface.feature
index 79499b96..f0efe4bc 100644
--- a/tests/acceptance/acceptance/PropertyAccessorInterface.feature
+++ b/tests/acceptance/acceptance/PropertyAccessorInterface.feature
@@ -1,4 +1,4 @@
-@symfony-common
+@symfony-7
Feature: PropertyAccessorInterface
Background:
@@ -9,6 +9,7 @@ Feature: PropertyAccessorInterface
setValue($company, 'name', 'Acme v2');
- array_key_exists('name', $company);
+ /** @psalm-trace $company */
"""
When I run Psalm
- Then I see no errors
+ Then I see these errors
+ | Type | Message |
+ | Trace | $company: array |
+ And I see no other errors
Scenario: Set value keeps object instance if an object is passed
Given I have the following code
@@ -36,28 +40,26 @@ Feature: PropertyAccessorInterface
$propertyAccessor->setValue($company, 'name', 'Acme v2');
- echo $company->name;
+ /** @psalm-trace $company */
"""
When I run Psalm
- Then I see no errors
+ Then I see these errors
+ | Type | Message |
+ | Trace | $company: Company |
+ And I see no other errors
Scenario: Set value does not modify the propertyAccessor variable
Given I have the following code
"""
- use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
-
class Company
{
- /**
- * @var PropertyAccessorInterface
- */
- private $propertyAccessor;
-
- public function __construct(PropertyAccessorInterface $propertyAccessor) {
- $this->propertyAccessor = $propertyAccessor;
+ public function __construct(
+ private PropertyAccessorInterface $propertyAccessor,
+ ) {
}
- public function doThings(Company $thing): void {
+ public function doThings(Company $thing): void
+ {
$this->propertyAccessor->setValue($thing, 'foo', 'bar');
$this->propertyAccessor->setValue($thing, 'foo', 'bar');
}
diff --git a/tests/acceptance/acceptance/Voter.feature b/tests/acceptance/acceptance/Voter.feature
deleted file mode 100644
index 2dca842d..00000000
--- a/tests/acceptance/acceptance/Voter.feature
+++ /dev/null
@@ -1,40 +0,0 @@
-@symfony-common
-Feature: Voter abstract class
-
- Background:
- Given I have Symfony plugin enabled
- And I have the following code preamble
- """
-
+ %env(bool:ENV_PARAM)%
+ %env(string:ENV_PARAM)%
+ %env(custom_type:ENV_PARAM)%
+ %env(json:ENV_PARAM)%
+ %env(enum:ENV_PARAM)%
+ %env(url:ENV_PARAM)%
@@ -46,7 +52,7 @@
-
+
@@ -66,7 +72,7 @@
-
+
@@ -84,7 +90,15 @@
+
+
+
+
+ kernel::registerContainerConfiguration()
+
+
+
diff --git a/tests/unit/Symfony/ContainerMetaTest.php b/tests/unit/Symfony/ContainerMetaTest.php
index 9f380d98..7120aabf 100644
--- a/tests/unit/Symfony/ContainerMetaTest.php
+++ b/tests/unit/Symfony/ContainerMetaTest.php
@@ -117,7 +117,32 @@ public function testGetParameter(): void
], $this->containerMeta->getParameter('nested_collection'));
}
- public function testGetParameterP(): void
+ /**
+ * @dataProvider guessParameterTypeProvider
+ */
+ public function testGuessParameterType(?array $expectedTypes, string $parameterName): void
+ {
+ $this->assertSame($expectedTypes, $this->containerMeta->guessParameterType($parameterName));
+ }
+
+ public function guessParameterTypeProvider(): iterable
+ {
+ yield [['string'], 'kernel.environment'];
+ yield [['bool'], 'debug_enabled'];
+ yield [['string'], 'version'];
+ yield [['int'], 'integer_one'];
+ yield [['float'], 'pi'];
+ yield [['array'], 'collection1'];
+ yield [['array'], 'nested_collection'];
+ yield [['bool'], 'env_param_bool'];
+ yield [['string'], 'env_param_string'];
+ yield [null, 'env_param_custom_type'];
+ yield [['array'], 'env_param_json'];
+ yield [['BackedEnum'], 'env_param_enum'];
+ yield [['array'], 'env_param_url'];
+ }
+
+ public function testGetParameterNonExistent(): void
{
$this->expectException(ParameterNotFoundException::class);
$this->containerMeta->getParameter('non_existent');