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

Allow mixed type dynamic constants #3583

Open
wants to merge 2 commits into
base: 2.0.x
Choose a base branch
from

Conversation

ssigwart
Copy link

'MixedTypeConstants\NoMixedTypeConstantClass::MIXED_TYPE_CONSTANT_IN_CLASS',
],
[
'false',
Copy link
Author

Choose a reason for hiding this comment

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

This doesn't seem right to me, but this test is copied and modified from testDynamicConstants where it also has false. That changed from bool to false in 285b49f#diff-52d2cce81a3daff27b7d6d8ba619b045420ab9a9acffc57c1c379af4eba0b127R8384.

I tried tracking down how I might be able to fix it. I found that the constant gets defined here:

$constantName = $scope->getType($node->getArgs()[0]->value);
if (
!$constantName instanceof ConstantStringType
|| $constantName->getValue() === ''
) {
return new SpecifiedTypes([], []);
}
return $this->typeSpecifier->create(
new Node\Expr\ConstFetch(
new Node\Name\FullyQualified($constantName->getValue()),
),
$scope->getType($node->getArgs()[1]->value),
TypeSpecifierContext::createTruthy(),
$scope,
)->setAlwaysOverwriteTypes();

The issue is that $scope->getType($node->getArgs()[1]->value) is just going based off the value, so it never reaches where I think it would determine that the constant is dynamic:

$constantType = $this->constantResolver->resolveConstant($node->name, $this);

@ondrejmirtes
Copy link
Member

It'd make sense to me to allow this syntax to force a specific constant type:

parameters:
	dynamicConstantNames:
		- DATABASE_ENGINE
		- Foo::BAR_CONSTANT
		ES_ENGINE: string|null
		Foo::BAZ_CONSTANT: mixed

@ssigwart
Copy link
Author

It'd make sense to me to allow this syntax to force a specific constant type:

parameters:
	dynamicConstantNames:
		- DATABASE_ENGINE
		- Foo::BAR_CONSTANT
		ES_ENGINE: string|null
		Foo::BAZ_CONSTANT: mixed

That makes sense. I can get it as far as detecting that it's in dynamicConstantNames and getting the string value (e.g. string|null for ES_ENGINE), but haven't figured out how ConstantResolver can take that string and convert it to a PHPStan\Type\Type. Any suggestions?

@staabm
Copy link
Contributor

staabm commented Oct 27, 2024

That makes sense. I can get it as far as detecting that it's in dynamicConstantNames and getting the string value (e.g. string|null for ES_ENGINE), but haven't figured out how ConstantResolver can take that string and convert it to a PHPStan\Type\Type. Any suggestions?

I think you can take inspiration from

private function getTypeFromString(string $typeString, ?string $className): Type
{
if ($typeString === '') {
return new MixedType(true);
}
return $this->typeStringResolver->resolve($typeString, new NameScope(null, [], $className));
}

@ssigwart
Copy link
Author

Thanks, @staabm! That got me on the right track. I got this working. I even fixed #3583 (comment). I'm not 100% sure if the new interfaces I added are desired, but it gets the job done. Please let me know if you want me to make any changes.

I could use some help with the 1 failing test though:
https://github.com/phpstan/phpstan-src/actions/runs/11542351640/job/32125482914?pr=3583

if ($constantType->isConstantValue()->yes()) {
if (array_key_exists($constantName, $this->dynamicConstantNames)) {
$phpdocTypes = $this->dynamicConstantNames[$constantName];
return $this->reflectionProviderProvider->getReflectionProvider()->getSignatureMapProvider()->getTypeFromString($phpdocTypes, null);
Copy link
Member

Choose a reason for hiding this comment

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

You should use TypeStringResolver for this, not SignatureMapProvider.

Copy link
Author

@ssigwart ssigwart Nov 8, 2024

Choose a reason for hiding this comment

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

How would I get an instance of TypeStringResolver from here?


if ($constantType->isConstantValue()->yes()) {
$phpdocTypes = $this->dynamicConstantNames[$lookupConstantName];
return $this->getReflectionProvider()->getSignatureMapProvider()->getTypeFromString($phpdocTypes, $className);
Copy link
Member

Choose a reason for hiding this comment

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

dtto

@@ -36,4 +37,6 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn

public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string;

public function getSignatureMapProvider(): SignatureMapProvider;
Copy link
Member

Choose a reason for hiding this comment

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

Don't add these methods please.

Addresses phpstan/phpstan#7520

Revert "Allow mixed type dynamic constants."

This reverts commit 1854985.

Allow setting types for dynamic constants

Unit test fixes

Unit test fix 2

Lint fixes

Use TypeStringResolver directly

Remove old code
@ssigwart
Copy link
Author

ssigwart commented Nov 8, 2024

Thanks, @ondrejmirtes. I made updates. I'm not sure if passing around the container is the right thing to do, but I got it working.

@@ -41,6 +43,7 @@ public function __construct(
private array $dynamicConstantNames,
private ?PhpVersion $composerMinPhpVersion,
private ?PhpVersion $composerMaxPhpVersion,
private ?Container $container,
Copy link
Contributor

@staabm staabm Nov 8, 2024

Choose a reason for hiding this comment

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

make this new param the TypeStringResolver itself.

It will be injected by the DIC automatically

Copy link
Author

Choose a reason for hiding this comment

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

I initially thought that might be the way to do it. However, when I tried it, I wound up getting an error about circular dependencies. So that's why I switched to the entire container. Do you have any ideas on how to get around the circular dependency error? It's also possible I just did it wrong. I assumed I needed to update the places that called the constructor, so I was using the container there to get the TypeStringResolver.

Copy link
Contributor

Choose a reason for hiding this comment

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

ah I can see it now.

TypeStringResolver has a TypeNodeResolver contruction dependency which in turn has a ConstantResolver dependency.

so when you add a TypeStringResolver dependency on ConstantResolver you build a cycle.

I will think about it and report back

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess one way we could break the cycle is to remove the ConstantResolver construction-time dependency of TypeNodeResolver and turn it into a setter injection (similar how we do it in TypeSpecifierFactory for the TypeSpecifier).

in my local testing I did not yet succeed in doing so :)

Copy link
Contributor

@staabm staabm Nov 8, 2024

Choose a reason for hiding this comment

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

or maybe better instead remove TypeNodeResolver from TypeStringResolver construction-time deps and move it to setter-injection

Copy link
Author

Choose a reason for hiding this comment

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

Sorry, @staabm. I don't have experience with DI in PHP, so I'm not entirely sure how to do this. I assume you meant to add a setTypeNodeResolver function to TypeStringResolver. I'm not sure where I would call setTypeNodeResolver though. I thought maybe I needed to add a TypeStringResolverFactory. I'm guessing that maybe I need to update conf/config.neon, so I changed it to this:

-
	class: PHPStan\PhpDoc\TypeStringResolver
typeStringResolverFactory:
	class: PHPStan\PhpDoc\TypeStringResolverFactory

In a new TypeStringResolverFactory class, I added this:

public function create(): TypeStringResolver
{
	$typeStringResolver = new TypeStringResolver(
		$this->typeLexer,
		$this->typeParser
	);
	$typeStringResolver->setTypeNodeResolver($this->container->getByType(TypeNodeResolver::class));

	return $typeStringResolver;
}

I still wound up with this when running the test though:

In Container.php line 327:

  Circular reference detected for: reflectionProvider, betterReflectionProvider, 093,
  058, 042, 040.

Am I on the right track? Do you have any suggestions? Thanks.

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

Successfully merging this pull request may close these issues.

3 participants