Skip to content
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

Merged
merged 16 commits into from
Nov 25, 2022

Conversation

tux-rampage
Copy link
Member

Q A
Documentation no
Bugfix no
BC Break no
New Feature yes
RFC no
QA yes

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.

@tux-rampage tux-rampage added this to the 3.3.0 milestone Oct 23, 2020
@tux-rampage tux-rampage self-assigned this Oct 23, 2020
@tux-rampage tux-rampage linked an issue Oct 23, 2020 that may be closed by this pull request
8 tasks
@tux-rampage
Copy link
Member Author

Note: Psalm cannot be installed as dependency with php 8 at the moment. See vimeo/psalm#4408

psalm.xml Outdated Show resolved Hide resolved
@weierophinney
Copy link
Member

@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 psalm.xml file to psalm.xml.dist (for consistency with other repos) and ensured an up-to-date composer.lock is available (built using PHP 7.4).

CS checks and Psalm checks are currently failing, which you can now see in the checks list.

@Ocramius Ocramius removed this from the 3.3.0 milestone Sep 21, 2021
@tux-rampage tux-rampage changed the base branch from 3.3.x to 3.4.x September 28, 2021 12:38
Copy link
Member Author

@tux-rampage tux-rampage left a 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",
Copy link
Member Author

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>
Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the most recent PHPUnit update, this must be enabled explicitly.
Is runtime deprecations still a thing? Can we drop them in favour of static analysis tools like psalm.

There are good reasons for not triggering deprecations at runtime. They're also a horror to maintain.

See #30 and #31

src/Config.php Show resolved Hide resolved
src/Config.php Show resolved Hide resolved
LegacyInjectorGenerator::class => CodeGenerator\InjectorGenerator::class,
'Zend\Di\InjectorInterface' => InjectorInterface::class,
'Zend\Di\ConfigInterface' => ConfigInterface::class,
'Zend\Di\CodeGenerator\InjectorGenerator' => CodeGenerator\InjectorGenerator::class,
Copy link
Member Author

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?

Copy link
Member

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

Copy link
Member Author

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());
Copy link
Member Author

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.

@tux-rampage tux-rampage added this to the 3.4.0 milestone Sep 28, 2021
@tux-rampage tux-rampage marked this pull request as ready for review September 28, 2021 15:45
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>
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

@tux-rampage tux-rampage Sep 28, 2021

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)

Copy link
Member

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?

Copy link
Member Author

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
Copy link
Member

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 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does

Copy link
Member Author

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,
Copy link
Member

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

@@ -36,7 +37,7 @@ private function getInjector(ContainerInterface $container)
/**
* Check creatability of the requested name
*
* @param string $requestedName
* @param string|Stringable $requestedName
Copy link
Member

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 :-\

Copy link
Member Author

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.

Copy link
Member Author

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);
Copy link
Member Author

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.

@tux-rampage
Copy link
Member Author

Thanks for your reviews @boesing and @Ocramius
I've addressed your feedback. Can you please re-check?

Copy link
Member

@boesing boesing left a 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"/>
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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" />
Copy link
Member

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.

Copy link
Member Author

@tux-rampage tux-rampage Nov 4, 2021

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> */
Copy link
Member

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.

Copy link
Member Author

@tux-rampage tux-rampage Nov 4, 2021

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.

Copy link
Member

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));
Copy link
Member

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.

https://github.com/laminas/technical-steering-committee/blob/main/meetings/minutes/2021-08-02-TSC-Minutes.md#allow-php-assert-along-with-webmozart-assert-in-specific-cases

Copy link
Member Author

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));
Copy link
Member

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.

https://github.com/laminas/technical-steering-committee/blob/main/meetings/minutes/2021-08-02-TSC-Minutes.md#allow-php-assert-along-with-webmozart-assert-in-specific-cases

Copy link
Member Author

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
Copy link
Member

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?

Copy link
Member Author

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
Copy link
Member

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?

Copy link
Member Author

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 */
Copy link
Member

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

Copy link
Member Author

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 */
Copy link
Member

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

Copy link
Member Author

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 $definition = [];

/** @var bool[] */
/** @var array<string, bool>|null */
Copy link
Member

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

Copy link
Member Author

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.

@tux-rampage
Copy link
Member Author

@boesing Thanks for taking time to review,

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.

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?

@Ocramius Ocramius removed their request for review February 15, 2022 07:30
@Ocramius
Copy link
Member

@tux-rampage a couple files require a rebase - nothing major, as we mostly only removed PHP 7.3 and locked everything with composer.lock.

I suggest moving most issues to Psalm's baseline and "closing the chapter" meanwhile :)

@tux-rampage
Copy link
Member Author

@tux-rampage a couple files require a rebase - nothing major, as we mostly only removed PHP 7.3 and locked everything with composer.lock.

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.

@Ocramius Ocramius removed this from the 3.4.0 milestone Feb 26, 2022
@Ocramius Ocramius removed this from the 3.5.0 milestone May 9, 2022
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
Signed-off-by: tux-rampage <tuxrampage@gmail.com>
@Ocramius Ocramius assigned Ocramius and unassigned tux-rampage Nov 25, 2022
@Ocramius Ocramius added this to the 3.11.0 milestone Nov 25, 2022
@Ocramius Ocramius changed the base branch from 3.5.x to 3.11.x November 25, 2022 09:16
`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.
@Ocramius
Copy link
Member

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!

@Ocramius Ocramius merged commit 45c9dfd into laminas:3.11.x Nov 25, 2022
@Ocramius Ocramius changed the title Add psalm static analysis Add psalm static analysis, improve type inference Nov 25, 2022
@tux-rampage
Copy link
Member Author

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. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Psalm integration
4 participants