-
-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add psalm static analysis, improve type inference #21
Conversation
Note: Psalm cannot be installed as dependency with php 8 at the moment. See vimeo/psalm#4408 |
d04a7cf
to
77afe34
Compare
@tux-rampage I've rebased your branch based on current 3.3.x, which includes the switch from Travis to GHA for CI purposes. I've also renamed the CS checks and Psalm checks are currently failing, which you can now see in the checks list. |
06e0d07
to
45f4176
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added as types as explicit as possible for now. I have decided to not work through all tests since this would make this already big PR even bigger and harder to review. Test-Code issues are added to the baseline. We can keep up with smaller PR to work these things out.
composer.json
Outdated
@@ -26,7 +26,7 @@ | |||
} | |||
}, | |||
"require": { | |||
"php": "^7.3 || ~8.0.0 || ~8.1.0", | |||
"php": "^7.4 || ~8.0.0 || ~8.1.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PHP 7.3 runs out of support this year. Frankly I'd drop PHP 7 entirely here and push to 8.0 and 8.1 (separate PR)
phpcs.xml
Outdated
@@ -22,10 +22,16 @@ | |||
<exclude-pattern>src/Resolver/AbstractInjection.php</exclude-pattern> | |||
</rule> | |||
|
|||
<!-- This must be excluded since it is not compatible with php < 8 --> | |||
<rule ref="SlevomatCodingStandard.Classes.ModernClassNameReference.ClassNameReferencedViaFunctionCal"> | |||
<exclude-pattern>*</exclude-pattern> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we still have to support 7.4 this rule must be excluded or test cases will fail.
$object::class
does not work with php < 8 and tests will fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add <config name="php_version" value="70499"/>
at the top instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect, that's way better than my exclude.
phpunit.xml.dist
Outdated
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" | ||
bootstrap="vendor/autoload.php" | ||
convertDeprecationsToExceptions="true" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LegacyInjectorGenerator::class => CodeGenerator\InjectorGenerator::class, | ||
'Zend\Di\InjectorInterface' => InjectorInterface::class, | ||
'Zend\Di\ConfigInterface' => ConfigInterface::class, | ||
'Zend\Di\CodeGenerator\InjectorGenerator' => CodeGenerator\InjectorGenerator::class, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still a thing? Could we drop this since this package is now marked as conflicting to zend-di
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really: these string keys were still strings, and not class aliases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a good idea on how to get rid of this gracefully?
{ | ||
$config = new Config($this->fixture); | ||
$this->assertEquals(['Foo', 'Bar'], $config->getConfiguredTypeNames()); | ||
$this->assertEquals([TestAsset\Config\SomeClass::class, 'SomeAlias'], $config->getConfiguredTypeNames()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All changes like this were necessary since we do now type with class-string
explicitly. This was caused especially by the typeOf
config key which always had to be an existing interface or class name (See docs).
It just revealed a logical bug that slipped due to lax typing.
phpcs.xml
Outdated
@@ -22,10 +22,16 @@ | |||
<exclude-pattern>src/Resolver/AbstractInjection.php</exclude-pattern> | |||
</rule> | |||
|
|||
<!-- This must be excluded since it is not compatible with php < 8 --> | |||
<rule ref="SlevomatCodingStandard.Classes.ModernClassNameReference.ClassNameReferencedViaFunctionCal"> | |||
<exclude-pattern>*</exclude-pattern> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add <config name="php_version" value="70499"/>
at the top instead
@@ -6,12 +6,16 @@ | |||
|
|||
use Psr\Container\ContainerInterface; | |||
|
|||
/** | |||
* @template T extends object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this work in inheritance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yepp should be no issue. The chance the this is implemented directly is almost zero.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, Implementing classes should use @template-implements
. I adjusted the tests for the generated classes.
Edit: See #21 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a working psalm annotation?
Shouldn't this be @template T of object
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, ith schould be of
. This was TypeScript comming through.
@@ -44,6 +49,8 @@ protected function ensureDirectory(string $dir) | |||
* | |||
* @throws LogicException | |||
* @throws GenerateCodeException | |||
* @return void | |||
* @psalm-assert non-empty-string $this->outputDirectory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure this works with private state 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it does
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you go: https://psalm.dev/r/205038dd2c
LegacyInjectorGenerator::class => CodeGenerator\InjectorGenerator::class, | ||
'Zend\Di\InjectorInterface' => InjectorInterface::class, | ||
'Zend\Di\ConfigInterface' => ConfigInterface::class, | ||
'Zend\Di\CodeGenerator\InjectorGenerator' => CodeGenerator\InjectorGenerator::class, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really: these string keys were still strings, and not class aliases
src/Container/AutowireFactory.php
Outdated
@@ -36,7 +37,7 @@ private function getInjector(ContainerInterface $container) | |||
/** | |||
* Check creatability of the requested name | |||
* | |||
* @param string $requestedName | |||
* @param string|Stringable $requestedName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooof, no, we should not really accept Stringable
:-\
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was just to keep BC with the config arrays. But I get your point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope you're right, this is actually extending the method signature which was not my intention.
Could we place a native string
typehint?
Some may argue that this would somehow be a BC break for user's who ignore the DocBlock and rely on the string cast behavior of this method.
Adding a native string
type hint is valid imo, since everything else already violates the existing contract from the DocBlock.
@@ -28,10 +31,15 @@ public function create(ContainerInterface $container, array $options = []) | |||
array_key_exists('anyDep', $options) ? $options['anyDep'] : null, | |||
]; | |||
|
|||
/** @psalm-suppress MixedArgument */ | |||
return new \LaminasTest\Di\TestAsset\Constructor\MixedArguments(...$args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The interface type would correctly be: public function create(ContainerInterface $container, PartialArray<ConstructArgsArray<T>> $options = []): T
This can only be achieved with a psalm plug-in as far as I know. But this is out of scope of this PR.
To avoid additional overhead in these generated classes, a suppress is added and responsibility is passed to PHP's type system until a better solution can be provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooof, well this PR has really plenty of code changes and its not that easy to review. However, I've added a few things which should be addressed.
Especially those null
properties which could be empty arrays to make things simpler.
phpcs.xml
Outdated
@@ -1,16 +1,17 @@ | |||
<?xml version="1.0"?> | |||
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> | |||
|
|||
<config name="php_version" value="70499"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this shouldn't be necessary as we've changed the CI matrix so that QA tooling (such as codestyle checks) are only executed on the minimum supported PHP version of a component.
As this component requires PHP 7.4 as its minimum, you can remove this line here so it won't lead to issues when removing PHP 7.4.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I guess this happened since I'm running php 8 locally and stumbled upon this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept this: still useful for local runs
psalm.xml.dist
Outdated
</issueHandlers> | ||
|
||
<stubs> | ||
<file name="./stubs/psr-container.phpstub" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer using lctrs/psalm-psr-container-plugin
but I know that this is a bit opinionated and thus, I know that @Ocramius does not like such magic.
But I'd suggest we go either with the existing psalm-plugin or not.
In both cases, we should drop the stub from this project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I agree with @Ocramius since all that Plugin does (as far as I got it) is re-implementing what psalm already can do with templates/generics (and therefore adds a greater potential to break or prevent a psalm upgrade).
Anyways I can replace it with that Plugin. Dropping this without a replacement would require additional typecheck code and make this already big PR even bigger.
Edit: Submitted this comment too soon.
@@ -13,10 +13,10 @@ | |||
*/ | |||
abstract class AbstractInjector implements InjectorInterface | |||
{ | |||
/** @var string[]|FactoryInterface[] */ | |||
/** @var array<string, class-string<FactoryInterface>|FactoryInterface> */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe bump minimum requirement of laminas-servicemanager
.
I've added plenty of psalm types there which can be imported here for re-usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
laminas-servicemanager is a dev-dependency and it's not meant to be a requirement.
Importing types from this project would require to make it a runtime dependency just to pass static analysis, which is too much imho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is tested against a recent and locked servicemanager: all good
@@ -49,6 +49,7 @@ private function buildFromTemplate(string $templateFile, string $outputFile, arr | |||
$template = file_get_contents($templateFile); | |||
|
|||
assert(is_string($template)); | |||
assert(is_string($this->outputDirectory)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imho, thats not safe. This is only true in case someone called the setter. Using php assert
in this case is not how it should be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. $this->ensureOutputDirectory()
would be correct here, which does provide the correct assert and the required directory existence checks / create attempts.
@@ -183,6 +183,8 @@ public function generate(string $class): string | |||
$factoryClassName = $this->namespace . '\\' . $this->buildClassName($class); | |||
[$namespace, $unqualifiedFactoryClassName] = $this->splitFullyQualifiedClassName($factoryClassName); | |||
|
|||
assert(is_string($this->outputDirectory)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imho, thats not safe. This is only true in case someone called the setter. Using php assert in this case is not how it should be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Same as for the AutoloadGenerator
.
@@ -69,7 +71,10 @@ public function getInterfaces(): array | |||
return $this->reflection->getInterfaceNames(); | |||
} | |||
|
|||
private function reflectParameters() | |||
/** | |||
* @psalm-assert array<string, Parameter> $this->parameters |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of this, wouldn't it be better to simply set parameters
to array with a default value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes, but this would require additional refactoring to this class since null
is used by getter methods. I just added the types to reflect the existing behaviour.
@@ -34,7 +33,10 @@ public function __construct($class) | |||
$this->reflection = $class; | |||
} | |||
|
|||
private function reflectSupertypes() | |||
/** | |||
* @psalm-assert list<string> $this->supertypes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of this, wouldn't it be better to simply set supertypes
to array with a default value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes, but this would require additional refactoring to this class since null
is used by getter methods. I just added the types to reflect the existing behaviour.
private $parameters; | ||
|
||
/** @var string[] */ | ||
/** @var list<string>|null */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if setting an empty array as default, we could avoid having null
here imho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes, but this would require additional refactoring to this class since null
is used by getter methods. I just added the types to reflect the existing behaviour.
@@ -16,14 +15,14 @@ class ClassDefinition implements ClassDefinitionInterface | |||
/** @var ReflectionClass */ | |||
private $reflection; | |||
|
|||
/** @var Parameter[] */ | |||
/** @var Parameter[]|null */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if setting an empty array as default, we could avoid having null
here imho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes, but this would require additional refactoring to this class since null
is used by getter methods. I just added the types to reflect the existing behaviour.
src/Definition/RuntimeDefinition.php
Outdated
private $definition = []; | ||
|
||
/** @var bool[] */ | ||
/** @var array<string, bool>|null */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if setting an empty array as default, we could avoid having null
here imho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes, but this would require additional refactoring to this class since null
is used by getter methods. I just added the types to reflect the existing behaviour.
@boesing Thanks for taking time to review,
Yes, that's why I focused on adding types for the existing implementation without refactoring where possible. You pointed out a lot of good and valid points to improve. I'd prefer to address the Improvements in separate smaller PRs and focus on the bugs you pointed out (like the incorrect string-asserts). As you noted, this one is already hard enough to review, and I don't want to add more complexity here as necessary. Would this be OK for you? |
@tux-rampage a couple files require a rebase - nothing major, as we mostly only removed PHP 7.3 and locked everything with I suggest moving most issues to Psalm's baseline and "closing the chapter" meanwhile :) |
Perfect agreed. I'd rather like to do this in smaller PRs that are easier to review. |
Signed-off-by: tux-rampage <[email protected]>
Signed-off-by: tux-rampage <[email protected]>
Signed-off-by: tux-rampage <[email protected]>
Signed-off-by: tux-rampage <[email protected]>
Signed-off-by: tux-rampage <[email protected]>
Signed-off-by: tux-rampage <[email protected]>
Signed-off-by: tux-rampage <[email protected]>
9212a79
to
5f5c199
Compare
`vimeo/psalm:^4` does not support native intersection types yet: `vimeo/psalm:^5` will do, but for now, we just exclude this code path from analysis.
Note: many reflection-related suppressions depend on vimeo/psalm#8722 Also, we preserved many assertions on `class-string`, since removing input validation would be a BC break.
After rebasing and many tiny adjustments (Psalm became MUCH more powerful over the last whole year, I decided that this is good to go as-is. Thanks @tux-rampage, and sorry for dragging this on: very valuable work! |
Thank you for finishing this. 👍 |
Description
As decided by the technical steering committee, components should be covered by psalm static analysis.
This PR attempts to replace phpstan with psalm.
It will also fix reported issues and provide support for templates/generics when possible.