Skip to content

Commit c1a266e

Browse files
authored
Check constants in parameters
1 parent 60b748d commit c1a266e

File tree

47 files changed

+3667
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3667
-2
lines changed

resources/constantToFunctionParameterMap.php

Lines changed: 2451 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection;
4+
5+
/**
6+
* Result of checking constants passed to a parameter against its allowed set.
7+
*
8+
* Returned by ExtendedParameterReflection::checkAllowedConstants(). Reports
9+
* three kinds of problems: constants not in the allowed list, mutually exclusive
10+
* constants combined in a bitmask, and bitmask usage on a single-value parameter.
11+
*
12+
* @api
13+
*/
14+
final class AllowedConstantsResult
15+
{
16+
17+
/**
18+
* @param list<ConstantReflection> $disallowedConstants
19+
* @param list<list<string>> $violatedExclusiveGroups
20+
*/
21+
public function __construct(
22+
private array $disallowedConstants,
23+
private array $violatedExclusiveGroups,
24+
private bool $bitmaskNotAllowed,
25+
)
26+
{
27+
}
28+
29+
public function isOk(): bool
30+
{
31+
return $this->disallowedConstants === [] && $this->violatedExclusiveGroups === [] && !$this->bitmaskNotAllowed;
32+
}
33+
34+
public function isBitmaskNotAllowed(): bool
35+
{
36+
return $this->bitmaskNotAllowed;
37+
}
38+
39+
/**
40+
* @return list<ConstantReflection>
41+
*/
42+
public function getDisallowedConstants(): array
43+
{
44+
return $this->disallowedConstants;
45+
}
46+
47+
/**
48+
* @return list<list<string>>
49+
*/
50+
public function getViolatedExclusiveGroups(): array
51+
{
52+
return $this->violatedExclusiveGroups;
53+
}
54+
55+
}

src/Reflection/Annotations/AnnotationsMethodParameterReflection.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace PHPStan\Reflection\Annotations;
44

5+
use PHPStan\Reflection\AllowedConstantsResult;
56
use PHPStan\Reflection\ExtendedParameterReflection;
7+
use PHPStan\Reflection\ParameterAllowedConstants;
68
use PHPStan\Reflection\PassedByReference;
79
use PHPStan\TrinaryLogic;
810
use PHPStan\Type\MixedType;
@@ -80,4 +82,14 @@ public function getAttributes(): array
8082
return [];
8183
}
8284

85+
public function getAllowedConstants(): ?ParameterAllowedConstants
86+
{
87+
return null;
88+
}
89+
90+
public function checkAllowedConstants(array $constants): AllowedConstantsResult
91+
{
92+
return new AllowedConstantsResult([], [], false);
93+
}
94+
8395
}

src/Reflection/ExtendedParameterReflection.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ public function getClosureThisType(): ?Type;
2929
*/
3030
public function getAttributes(): array;
3131

32+
public function getAllowedConstants(): ?ParameterAllowedConstants;
33+
34+
/**
35+
* @param list<ConstantReflection> $constants Global and/or class constant reflections
36+
*/
37+
public function checkAllowedConstants(array $constants): AllowedConstantsResult;
38+
3239
}

src/Reflection/GenericParametersAcceptorResolver.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
105105
TrinaryLogic::createMaybe(),
106106
null,
107107
[],
108+
null,
108109
), $parameters),
109110
$parametersAcceptor->isVariadic(),
110111
$returnType,

src/Reflection/Native/ExtendedNativeParameterReflection.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace PHPStan\Reflection\Native;
44

5+
use PHPStan\Reflection\AllowedConstantsResult;
56
use PHPStan\Reflection\AttributeReflection;
67
use PHPStan\Reflection\ExtendedParameterReflection;
8+
use PHPStan\Reflection\ParameterAllowedConstants;
79
use PHPStan\Reflection\PassedByReference;
810
use PHPStan\TrinaryLogic;
911
use PHPStan\Type\MixedType;
@@ -28,6 +30,7 @@ public function __construct(
2830
private TrinaryLogic $immediatelyInvokedCallable,
2931
private ?Type $closureThisType,
3032
private array $attributes,
33+
private ?ParameterAllowedConstants $allowedConstants,
3134
)
3235
{
3336
}
@@ -97,4 +100,18 @@ public function getAttributes(): array
97100
return $this->attributes;
98101
}
99102

103+
public function getAllowedConstants(): ?ParameterAllowedConstants
104+
{
105+
return $this->allowedConstants;
106+
}
107+
108+
public function checkAllowedConstants(array $constants): AllowedConstantsResult
109+
{
110+
if ($this->allowedConstants === null) {
111+
return new AllowedConstantsResult([], [], false);
112+
}
113+
114+
return $this->allowedConstants->check($constants);
115+
}
116+
100117
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection;
4+
5+
use function count;
6+
use function in_array;
7+
8+
/**
9+
* Describes which constants a function/method parameter accepts.
10+
*
11+
* Parameters are either 'single' (exactly one constant, e.g. `array_unique($flags)`)
12+
* or 'bitmask' (constants combinable with `|`, e.g. `json_encode($flags)`).
13+
* Bitmask parameters may have exclusive groups — subsets of constants
14+
* that are mutually exclusive even within a bitmask.
15+
*
16+
* Populated from resources/constantToFunctionParameterMap.php and
17+
* available via ExtendedParameterReflection::getAllowedConstants().
18+
*
19+
* @api
20+
*/
21+
final class ParameterAllowedConstants
22+
{
23+
24+
/**
25+
* @param 'single'|'bitmask' $type
26+
* @param list<string> $constants
27+
* @param list<list<string>> $exclusiveGroups
28+
*/
29+
public function __construct(
30+
private string $type,
31+
private array $constants,
32+
private array $exclusiveGroups,
33+
)
34+
{
35+
}
36+
37+
public function isBitmask(): bool
38+
{
39+
return $this->type === 'bitmask';
40+
}
41+
42+
/**
43+
* @return list<list<string>>
44+
*/
45+
public function getExclusiveGroups(): array
46+
{
47+
return $this->exclusiveGroups;
48+
}
49+
50+
private function resolveConstantName(ConstantReflection $constant): string
51+
{
52+
if ($constant instanceof ClassConstantReflection) {
53+
return $constant->getDeclaringClass()->getName() . '::' . $constant->getName();
54+
}
55+
56+
return $constant->getName();
57+
}
58+
59+
/**
60+
* @param list<ConstantReflection> $constants
61+
*/
62+
public function check(array $constants): AllowedConstantsResult
63+
{
64+
$bitmaskNotAllowed = !$this->isBitmask() && count($constants) > 1;
65+
66+
$disallowed = [];
67+
$names = [];
68+
69+
foreach ($constants as $constant) {
70+
$name = $this->resolveConstantName($constant);
71+
$names[] = $name;
72+
73+
if (in_array($name, $this->constants, true)) {
74+
continue;
75+
}
76+
77+
$disallowed[] = $constant;
78+
}
79+
80+
$violated = [];
81+
if ($this->isBitmask()) {
82+
foreach ($this->exclusiveGroups as $group) {
83+
$matched = [];
84+
foreach ($names as $name) {
85+
if (!in_array($name, $group, true)) {
86+
continue;
87+
}
88+
89+
$matched[] = $name;
90+
}
91+
92+
if (count($matched) < 2) {
93+
continue;
94+
}
95+
96+
$violated[] = $matched;
97+
}
98+
}
99+
100+
return new AllowedConstantsResult($disallowed, $violated, $bitmaskNotAllowed);
101+
}
102+
103+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection;
4+
5+
use PHPStan\DependencyInjection\AutowiredService;
6+
7+
#[AutowiredService]
8+
final class ParameterAllowedConstantsMapProvider
9+
{
10+
11+
/** @var array<string, array<string, array{type: string, constants: list<string>, exclusiveGroups?: list<list<string>>}>>|null */
12+
private ?array $map = null;
13+
14+
public function getForFunctionParameter(string $functionName, string $parameterName): ?ParameterAllowedConstants
15+
{
16+
return $this->get($functionName, $parameterName);
17+
}
18+
19+
public function getForMethodParameter(string $className, string $methodName, string $parameterName): ?ParameterAllowedConstants
20+
{
21+
return $this->get($className . '::' . $methodName, $parameterName);
22+
}
23+
24+
private function get(string $key, string $parameterName): ?ParameterAllowedConstants
25+
{
26+
$map = $this->getMap();
27+
28+
if (!isset($map[$key][$parameterName])) {
29+
return null;
30+
}
31+
32+
/** @var array{type: 'single'|'bitmask', constants: list<string>, exclusiveGroups?: list<list<string>>} $config */
33+
$config = $map[$key][$parameterName];
34+
35+
return new ParameterAllowedConstants(
36+
$config['type'],
37+
$config['constants'],
38+
$config['exclusiveGroups'] ?? [],
39+
);
40+
}
41+
42+
/**
43+
* @return array<string, array<string, array{type: string, constants: list<string>, exclusiveGroups?: list<list<string>>}>>
44+
*/
45+
private function getMap(): array
46+
{
47+
return $this->map ??= require __DIR__ . '/../../resources/constantToFunctionParameterMap.php';
48+
}
49+
50+
}

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
771771
$parameter instanceof ExtendedParameterReflection ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(),
772772
$parameter instanceof ExtendedParameterReflection ? $parameter->getClosureThisType() : null,
773773
$parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [],
774+
$parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null,
774775
);
775776
continue;
776777
}
@@ -830,6 +831,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
830831
$immediatelyInvokedCallable,
831832
$closureThisType,
832833
$attributes,
834+
null,
833835
);
834836

835837
if ($isVariadic) {
@@ -928,6 +930,7 @@ private static function wrapParameter(ParameterReflection $parameter): ExtendedP
928930
TrinaryLogic::createMaybe(),
929931
null,
930932
[],
933+
null,
931934
);
932935
}
933936

src/Reflection/Php/ClosureCallMethodReflection.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public function getVariants(): array
9898
TrinaryLogic::createMaybe(),
9999
null,
100100
[],
101+
null,
101102
), $parameters),
102103
$this->closureType->isVariadic(),
103104
$this->closureType->getReturnType(),

0 commit comments

Comments
 (0)