Skip to content

Commit ea0efcc

Browse files
Fix FilterFunctionReturnTypeHelper
1 parent 9dbff97 commit ea0efcc

File tree

5 files changed

+93
-36
lines changed

5 files changed

+93
-36
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
strategy:
3131
fail-fast: false
3232
matrix:
33+
php-version: ["8.2"]
3334
include:
3435
- script: |
3536
cd e2e/result-cache-1
@@ -282,6 +283,10 @@ jobs:
282283
cd e2e/ignore-error-extension
283284
composer install
284285
../../bin/phpstan
286+
- script: |
287+
cd e2e/bug10483
288+
../../bin/phpstan -vvv
289+
php-version: "7.2"
285290
286291
steps:
287292
- name: "Checkout"
@@ -291,7 +296,7 @@ jobs:
291296
uses: "shivammathur/setup-php@v2"
292297
with:
293298
coverage: "none"
294-
php-version: "8.2"
299+
php-version: ${{ matrix.php-version }}
295300
extensions: mbstring
296301
ini-values: memory_limit=256M
297302

@@ -408,6 +413,11 @@ jobs:
408413
- name: "Install dependencies"
409414
run: "composer install --no-interaction --no-progress"
410415

416+
- name: "Transform source code"
417+
if: matrix.php-version == '7.2'
418+
shell: bash
419+
run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}"
420+
411421
- name: "Install bashunit"
412422
run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.22.0"
413423

e2e/bug-10483/bootstrap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
// constant that's used in the Filter extension that was introduced in a later version of PHP.
4+
// on earlier php version introduce the same constant via a bootstrap file but with a wrong type
5+
if(!defined("FILTER_SANITIZE_ADD_SLASHES"))define("FILTER_SANITIZE_ADD_SLASHES",false);

e2e/bug-10483/bug-10483.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
function doFoo(mixed $filter): void {
4+
\PHPStan\Testing\assertType('non-falsy-string|false', filter_var("no", FILTER_VALIDATE_REGEXP));
5+
}

e2e/bug-10483/phpstan.dist.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
level: 9
3+
paths:
4+
- src
5+
6+
bootstrapFiles:
7+
- bootstrap.php

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv
5858

5959
public function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type
6060
{
61-
$inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType)
61+
$inexistentOffsetType = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
6262
? new ConstantBooleanType(false)
6363
: new NullType();
6464

@@ -107,6 +107,9 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
107107

108108
if ($filterType === null) {
109109
$filterValue = $this->getConstant('FILTER_DEFAULT');
110+
if (null === $filterValue) {
111+
return $mixedType;
112+
}
110113
} else {
111114
if (!$filterType instanceof ConstantIntegerType) {
112115
return $mixedType;
@@ -121,17 +124,17 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
121124
$hasOptions = $this->hasOptions($flagsType);
122125
$options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : [];
123126

124-
$defaultType = $options['default'] ?? ($this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType)
127+
$defaultType = $options['default'] ?? ($this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
125128
? new NullType()
126129
: new ConstantBooleanType(false));
127130

128131
$inputIsArray = $inputType->isArray();
129-
$hasRequireArrayFlag = $this->hasFlag($this->getConstant('FILTER_REQUIRE_ARRAY'), $flagsType);
132+
$hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType);
130133
if ($inputIsArray->no() && $hasRequireArrayFlag) {
131134
return $defaultType;
132135
}
133136

134-
$hasForceArrayFlag = $this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType);
137+
$hasForceArrayFlag = $this->hasFlag('FILTER_FORCE_ARRAY', $flagsType);
135138
if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) {
136139
$inputArrayKeyType = $inputType->getIterableKeyType();
137140
$inputType = $inputType->getIterableValueType();
@@ -187,32 +190,46 @@ private function getFilterTypeMap(): array
187190
$stringType = new StringType();
188191
$nonFalsyStringType = TypeCombinator::intersect($stringType, new AccessoryNonFalsyStringType());
189192

190-
$this->filterTypeMap = [
191-
$this->getConstant('FILTER_UNSAFE_RAW') => $stringType,
192-
$this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType,
193-
$this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType,
194-
$this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType,
195-
$this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType,
196-
$this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType,
197-
$this->getConstant('FILTER_SANITIZE_STRING') => $stringType,
198-
$this->getConstant('FILTER_SANITIZE_URL') => $stringType,
199-
$this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType,
200-
$this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType,
201-
$this->getConstant('FILTER_VALIDATE_EMAIL') => $nonFalsyStringType,
202-
$this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType,
203-
$this->getConstant('FILTER_VALIDATE_INT') => $intType,
204-
$this->getConstant('FILTER_VALIDATE_IP') => $nonFalsyStringType,
205-
$this->getConstant('FILTER_VALIDATE_MAC') => $nonFalsyStringType,
206-
$this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType,
207-
$this->getConstant('FILTER_VALIDATE_URL') => $nonFalsyStringType,
193+
$map = [
194+
'FILTER_UNSAFE_RAW' => $stringType,
195+
'FILTER_SANITIZE_EMAIL' => $stringType,
196+
'FILTER_SANITIZE_ENCODED' => $stringType,
197+
'FILTER_SANITIZE_NUMBER_FLOAT' => $stringType,
198+
'FILTER_SANITIZE_NUMBER_INT' => $stringType,
199+
'FILTER_SANITIZE_SPECIAL_CHARS' => $stringType,
200+
'FILTER_SANITIZE_STRING' => $stringType,
201+
'FILTER_SANITIZE_URL' => $stringType,
202+
'FILTER_VALIDATE_BOOLEAN' => $booleanType,
203+
'FILTER_VALIDATE_DOMAIN' => $stringType,
204+
'FILTER_VALIDATE_EMAIL' => $nonFalsyStringType,
205+
'FILTER_VALIDATE_FLOAT' => $floatType,
206+
'FILTER_VALIDATE_INT' => $intType,
207+
'FILTER_VALIDATE_IP' => $nonFalsyStringType,
208+
'FILTER_VALIDATE_MAC' => $nonFalsyStringType,
209+
'FILTER_VALIDATE_REGEXP' => $stringType,
210+
'FILTER_VALIDATE_URL' => $nonFalsyStringType,
208211
];
209212

213+
$this->filterTypeMap = [];
214+
foreach ($map as $filter => $type) {
215+
$constant = $this->getConstant($filter);
216+
if (null !== $constant) {
217+
$this->filterTypeMap[$constant] = $type;
218+
}
219+
}
220+
210221
if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) {
211-
$this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType;
222+
$sanitizeMagicQuote = $this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES');
223+
if (null !== $sanitizeMagicQuote) {
224+
$this->filterTypeMap[$sanitizeMagicQuote] = $stringType;
225+
}
212226
}
213227

214228
if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) {
215-
$this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType;
229+
$sanitizeAddSlashes = $this->getConstant('FILTER_SANITIZE_ADD_SLASHES');
230+
if (null !== $sanitizeAddSlashes) {
231+
$this->filterTypeMap[$sanitizeAddSlashes] = $stringType;
232+
}
216233
}
217234

218235
return $this->filterTypeMap;
@@ -227,24 +244,32 @@ private function getFilterTypeOptions(): array
227244
return $this->filterTypeOptions;
228245
}
229246

230-
$this->filterTypeOptions = [
231-
$this->getConstant('FILTER_VALIDATE_INT') => ['min_range', 'max_range'],
247+
$map = [
248+
'FILTER_VALIDATE_INT' => ['min_range', 'max_range'],
232249
// PHPStan does not yet support FloatRangeType
233-
// $this->getConstant('FILTER_VALIDATE_FLOAT') => ['min_range', 'max_range'],
250+
// 'FILTER_VALIDATE_FLOAT' => ['min_range', 'max_range'],
234251
];
235252

253+
$this->filterTypeOptions = [];
254+
foreach ($map as $filter => $type) {
255+
$constant = $this->getConstant($filter);
256+
if (null !== $constant) {
257+
$this->filterTypeOptions[$constant] = $type;
258+
}
259+
}
260+
236261
return $this->filterTypeOptions;
237262
}
238263

239264
/**
240265
* @param non-empty-string $constantName
241266
*/
242-
private function getConstant(string $constantName): int
267+
private function getConstant(string $constantName): ?int
243268
{
244269
$constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null);
245270
$valueType = $constant->getValueType();
246271
if (!$valueType instanceof ConstantIntegerType) {
247-
throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName));
272+
return null;
248273
}
249274

250275
return $valueType->getValue();
@@ -301,8 +326,8 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
301326

302327
if ($in instanceof ConstantStringType) {
303328
$value = $in->getValue();
304-
$allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType);
305-
$allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType);
329+
$allowOctal = $this->hasFlag('FILTER_FLAG_ALLOW_OCTAL', $flagsType);
330+
$allowHex = $this->hasFlag('FILTER_FLAG_ALLOW_HEX', $flagsType);
306331

307332
if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
308333
$octalValue = octdec($value);
@@ -411,8 +436,13 @@ private function getOptions(Type $flagsType, int $filterValue): array
411436
return $options;
412437
}
413438

414-
private function hasFlag(int $flag, ?Type $flagsType): bool
439+
private function hasFlag(string $flagName, ?Type $flagsType): bool
415440
{
441+
$flag = $this->getConstant($flagName);
442+
if (null === $flag) {
443+
return false;
444+
}
445+
416446
if ($flagsType === null) {
417447
return false;
418448
}
@@ -441,9 +471,9 @@ private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool
441471
// FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW,
442472
// FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK
443473
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
444-
return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsType)
445-
|| $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsType)
446-
|| $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsType);
474+
return $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType)
475+
|| $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType)
476+
|| $this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType);
447477
}
448478

449479
return true;

0 commit comments

Comments
 (0)