Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ parameters:
curlSetOptArrayTypes: true
checkDateIntervalConstructor: true
reportMethodPurityOverride: true
checkDynamicConstantNameValues: true
10 changes: 10 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@ conditionalTags:
phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag%
PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension:
phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag%
PHPStan\Rules\Constants\ValueAssignedToDefineRule:
phpstan.rules.rule: %featureToggles.checkDynamicConstantNameValues%
PHPStan\Rules\Constants\ValueAssignedToGlobalConstantRule:
phpstan.rules.rule: %featureToggles.checkDynamicConstantNameValues%

services:
-
class: PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension

-
class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension

-
class: PHPStan\Rules\Constants\ValueAssignedToDefineRule

-
class: PHPStan\Rules\Constants\ValueAssignedToGlobalConstantRule
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ parameters:
curlSetOptArrayTypes: false
checkDateIntervalConstructor: false
reportMethodPurityOverride: false
checkDynamicConstantNameValues: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ parametersSchema:
curlSetOptArrayTypes: bool()
checkDateIntervalConstructor: bool()
reportMethodPurityOverride: bool()
checkDynamicConstantNameValues: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
58 changes: 48 additions & 10 deletions src/Analyser/ConstantResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ final class ConstantResolver
/** @var array<string, true> */
private array $currentlyResolving = [];

/** @var array<string, Type|null> */
private array $configuredTypesCache = [];

/**
* @param string[] $dynamicConstantNames
* @param int|array{min: int, max: int}|null $phpVersion
Expand Down Expand Up @@ -414,16 +417,52 @@ private function getMaxPhpVersion(): ?PhpVersion
return $this->composerPhpVersionFactory->getMaxVersion();
}

public function getConfiguredGlobalConstantType(string $constantName): ?Type
{
if (array_key_exists($constantName, $this->configuredTypesCache)) {
return $this->configuredTypesCache[$constantName];
}

$result = null;
if (array_key_exists($constantName, $this->dynamicConstantNames)) {
$phpdocTypes = $this->dynamicConstantNames[$constantName];
if ($this->container !== null) {
$typeStringResolver = $this->container->getByType(TypeStringResolver::class);
$result = $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], className: null));
}
Comment on lines +428 to +432
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cache resolved types locally, so we don't need to re-resolve them over and over again

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Added a configuredTypesCache property to ConstantResolver that caches the resolved Type objects from getConfiguredGlobalConstantType() and getConfiguredClassConstantType(), so repeated calls with the same constant name return the cached result instead of re-resolving the type string through TypeStringResolver each time. All 12077 tests pass and make phpstan reports no errors.

}

$this->configuredTypesCache[$constantName] = $result;

return $result;
}

public function getConfiguredClassConstantType(string $className, string $constantName): ?Type
{
$lookupConstantName = sprintf('%s::%s', $className, $constantName);
if (array_key_exists($lookupConstantName, $this->configuredTypesCache)) {
return $this->configuredTypesCache[$lookupConstantName];
}

$result = null;
if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) {
$phpdocTypes = $this->dynamicConstantNames[$lookupConstantName];
if ($this->container !== null) {
$typeStringResolver = $this->container->getByType(TypeStringResolver::class);
$result = $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className));
}
}

$this->configuredTypesCache[$lookupConstantName] = $result;

return $result;
}

public function resolveConstantType(string $constantName, Type $constantType): Type
{
if ($constantType->isConstantValue()->yes()) {
if (array_key_exists($constantName, $this->dynamicConstantNames)) {
$phpdocTypes = $this->dynamicConstantNames[$constantName];
if ($this->container !== null) {
$typeStringResolver = $this->container->getByType(TypeStringResolver::class);
return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], className: null));
}
return $constantType;
return $this->getConfiguredGlobalConstantType($constantName) ?? $constantType;
}
if (in_array($constantName, $this->dynamicConstantNames, true)) {
return $this->generalizeDynamicConstantType($constantType);
Expand All @@ -438,10 +477,9 @@ public function resolveClassConstantType(string $className, string $constantName
$lookupConstantName = sprintf('%s::%s', $className, $constantName);
if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) {
if ($constantType->isConstantValue()->yes()) {
$phpdocTypes = $this->dynamicConstantNames[$lookupConstantName];
if ($this->container !== null) {
$typeStringResolver = $this->container->getByType(TypeStringResolver::class);
return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className));
$explicitType = $this->getConfiguredClassConstantType($className, $constantName);
if ($explicitType !== null) {
return $explicitType;
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/Rules/Constants/ValueAssignedToClassConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace PHPStan\Rules\Constants;

use PhpParser\Node;
use PHPStan\Analyser\ConstantResolver;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\IdentifierRuleError;
Expand All @@ -23,6 +25,14 @@
final class ValueAssignedToClassConstantRule implements Rule
{

public function __construct(
private ConstantResolver $constantResolver,
#[AutowiredParameter(ref: '%featureToggles.checkDynamicConstantNameValues%')]
private bool $checkDynamicConstantNameValues,
)
{
}

public function getNodeType(): string
{
return Node\Stmt\ClassConst::class;
Expand Down Expand Up @@ -62,6 +72,25 @@ private function processSingleConstant(ClassReflection $classReflection, string
$phpDocType = $constantReflection->getPhpDocType();
if ($phpDocType === null) {
if ($nativeType === null) {
if (!$this->checkDynamicConstantNameValues) {
return [];
}
$configuredType = $this->constantResolver->getConfiguredClassConstantType($classReflection->getName(), $constantName);
if ($configuredType !== null) {
$accepts = $configuredType->accepts($valueExprType, true);
if (!$accepts->yes()) {
Comment thread
staabm marked this conversation as resolved.
$verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueExprType);
return [
RuleErrorBuilder::message(sprintf(
'Configuration defined type for constant %s::%s (%s) does not accept value %s.',
$constantReflection->getDeclaringClass()->getDisplayName(),
$constantName,
$configuredType->describe(VerbosityLevel::typeOnly()),
$valueExprType->describe($verbosity),
))->acceptsReasonsTip($accepts->reasons)->identifier('classConstant.value')->build(),
];
}
}
Comment thread
staabm marked this conversation as resolved.
return [];
}

Expand Down
83 changes: 83 additions & 0 deletions src/Rules/Constants/ValueAssignedToDefineRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Constants;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\ConstantResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function count;
use function sprintf;
use function strtolower;

/**
* @implements Rule<FuncCall>
*/
final class ValueAssignedToDefineRule implements Rule
{

public function __construct(private ConstantResolver $constantResolver)
{
}

public function getNodeType(): string
{
return FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Node\Name)) {
return [];
}

if (strtolower((string) $node->name) !== 'define') {
return [];
}

$args = $node->getArgs();
if (count($args) < 2) {
return [];
}

$constantNameStrings = $scope->getType($args[0]->value)->getConstantStrings();
if (count($constantNameStrings) === 0) {
return [];
}

$valueType = $scope->getType($args[1]->value);
$errors = [];

foreach ($constantNameStrings as $constantNameString) {
$constantName = $constantNameString->getValue();
if ($constantName === '') {
continue;
}

$configuredType = $this->constantResolver->getConfiguredGlobalConstantType($constantName);
if ($configuredType === null) {
continue;
}

$accepts = $configuredType->accepts($valueType, true);
if ($accepts->yes()) {
continue;
}

$verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType);

$errors[] = RuleErrorBuilder::message(sprintf(
'Configuration defined type for constant %s (%s) does not accept value %s.',
$constantName,
$configuredType->describe(VerbosityLevel::typeOnly()),
$valueType->describe($verbosity),
))->acceptsReasonsTip($accepts->reasons)->identifier('constant.defineValue')->build();
}

return $errors;
}

}
63 changes: 63 additions & 0 deletions src/Rules/Constants/ValueAssignedToGlobalConstantRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Constants;

use PhpParser\Node;
use PHPStan\Analyser\ConstantResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function sprintf;

/**
* @implements Rule<Node\Stmt\Const_>
*/
final class ValueAssignedToGlobalConstantRule implements Rule
Comment thread
staabm marked this conversation as resolved.
{

public function __construct(private ConstantResolver $constantResolver)
{
}

public function getNodeType(): string
{
return Node\Stmt\Const_::class;
}

public function processNode(Node $node, Scope $scope): array
{
$errors = [];

foreach ($node->consts as $const) {
if ($const->namespacedName !== null) {
$constantName = $const->namespacedName->toString();
} else {
$constantName = $const->name->toString();
}

$configuredType = $this->constantResolver->getConfiguredGlobalConstantType($constantName);
if ($configuredType === null) {
continue;
}

$valueType = $scope->getType($const->value);
$accepts = $configuredType->accepts($valueType, true);
if ($accepts->yes()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add a test to cover this path when $accepts is Maybe

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Added a test covering the Maybe accepts path in ValueAssignedToGlobalConstantRule. The test defines DYNAMIC_INT_CONSTANT (configured as int in dynamicConstantNames) in a separate file, then uses const MAYBE_CONSTANT = DYNAMIC_INT_CONSTANT; where MAYBE_CONSTANT is configured as positive-int. Since positive-int doesn't fully accept int, accepts() returns Maybe, exercising the !$accepts->yes() branch. All 12077 tests pass and make phpstan reports no errors.

continue;
}

$verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType);

$errors[] = RuleErrorBuilder::message(sprintf(
'Configuration defined type for constant %s (%s) does not accept value %s.',
$constantName,
$configuredType->describe(VerbosityLevel::typeOnly()),
$valueType->describe($verbosity),
))->acceptsReasonsTip($accepts->reasons)->identifier('constant.value')->build();
}

return $errors;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Constants;

use PHPStan\Analyser\ConstantResolver;
use PHPStan\Rules\Rule as TRule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;
Expand All @@ -14,7 +15,10 @@ class ValueAssignedToClassConstantRuleTest extends RuleTestCase

protected function getRule(): TRule
{
return new ValueAssignedToClassConstantRule();
return new ValueAssignedToClassConstantRule(
self::getContainer()->getByType(ConstantResolver::class),
false,
);
}

public function testRule(): void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Constants;

use PHPStan\Analyser\ConstantResolver;
use PHPStan\Rules\Rule as TRule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ValueAssignedToClassConstantRule>
*/
class ValueAssignedToClassConstantWithDynamicNamesRuleTest extends RuleTestCase
{

protected function getRule(): TRule
{
return new ValueAssignedToClassConstantRule(
self::getContainer()->getByType(ConstantResolver::class),
true,
);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/value-assigned-dynamic-constant.neon',
];
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-dynamic-names.php'], [
[
'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) does not accept value false.',
12,
],
[
'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::MAYBE_BAR (int<1, max>) does not accept value int.',
14,
],
]);
}

}
Loading
Loading