diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9f9e1376c6..af71d5fbf0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -282,6 +282,9 @@ jobs: cd e2e/ignore-error-extension composer install ../../bin/phpstan + - script: | + cd e2e/bug-10483 + ../../bin/phpstan steps: - name: "Checkout" diff --git a/e2e/bug-10483/bug-10483.php b/e2e/bug-10483/bug-10483.php new file mode 100644 index 0000000000..df3867ef78 --- /dev/null +++ b/e2e/bug-10483/bug-10483.php @@ -0,0 +1,10 @@ +hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + $inexistentOffsetType = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType) ? new ConstantBooleanType(false) : new NullType(); @@ -107,6 +105,9 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T if ($filterType === null) { $filterValue = $this->getConstant('FILTER_DEFAULT'); + if ($filterValue === null) { + return $mixedType; + } } else { if (!$filterType instanceof ConstantIntegerType) { return $mixedType; @@ -121,17 +122,17 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $hasOptions = $this->hasOptions($flagsType); $options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : []; - $defaultType = $options['default'] ?? ($this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + $defaultType = $options['default'] ?? ($this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType) ? new NullType() : new ConstantBooleanType(false)); $inputIsArray = $inputType->isArray(); - $hasRequireArrayFlag = $this->hasFlag($this->getConstant('FILTER_REQUIRE_ARRAY'), $flagsType); + $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); if ($inputIsArray->no() && $hasRequireArrayFlag) { return $defaultType; } - $hasForceArrayFlag = $this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType); + $hasForceArrayFlag = $this->hasFlag('FILTER_FORCE_ARRAY', $flagsType); if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) { $inputArrayKeyType = $inputType->getIterableKeyType(); $inputType = $inputType->getIterableValueType(); @@ -187,32 +188,47 @@ private function getFilterTypeMap(): array $stringType = new StringType(); $nonFalsyStringType = TypeCombinator::intersect($stringType, new AccessoryNonFalsyStringType()); - $this->filterTypeMap = [ - $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, - $this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType, - $this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType, - $this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType, - $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, - $this->getConstant('FILTER_SANITIZE_URL') => $stringType, - $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, - $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, - $this->getConstant('FILTER_VALIDATE_EMAIL') => $nonFalsyStringType, - $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, - $this->getConstant('FILTER_VALIDATE_INT') => $intType, - $this->getConstant('FILTER_VALIDATE_IP') => $nonFalsyStringType, - $this->getConstant('FILTER_VALIDATE_MAC') => $nonFalsyStringType, - $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, - $this->getConstant('FILTER_VALIDATE_URL') => $nonFalsyStringType, + $map = [ + 'FILTER_UNSAFE_RAW' => $stringType, + 'FILTER_SANITIZE_EMAIL' => $stringType, + 'FILTER_SANITIZE_ENCODED' => $stringType, + 'FILTER_SANITIZE_NUMBER_FLOAT' => $stringType, + 'FILTER_SANITIZE_NUMBER_INT' => $stringType, + 'FILTER_SANITIZE_SPECIAL_CHARS' => $stringType, + 'FILTER_SANITIZE_STRING' => $stringType, + 'FILTER_SANITIZE_URL' => $stringType, + 'FILTER_VALIDATE_BOOLEAN' => $booleanType, + 'FILTER_VALIDATE_DOMAIN' => $stringType, + 'FILTER_VALIDATE_EMAIL' => $nonFalsyStringType, + 'FILTER_VALIDATE_FLOAT' => $floatType, + 'FILTER_VALIDATE_INT' => $intType, + 'FILTER_VALIDATE_IP' => $nonFalsyStringType, + 'FILTER_VALIDATE_MAC' => $nonFalsyStringType, + 'FILTER_VALIDATE_REGEXP' => $stringType, + 'FILTER_VALIDATE_URL' => $nonFalsyStringType, ]; + $this->filterTypeMap = []; + foreach ($map as $filter => $type) { + $constant = $this->getConstant($filter); + if ($constant === null) { + continue; + } + $this->filterTypeMap[$constant] = $type; + } + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType; + $sanitizeMagicQuote = $this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES'); + if ($sanitizeMagicQuote !== null) { + $this->filterTypeMap[$sanitizeMagicQuote] = $stringType; + } } if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType; + $sanitizeAddSlashes = $this->getConstant('FILTER_SANITIZE_ADD_SLASHES'); + if ($sanitizeAddSlashes !== null) { + $this->filterTypeMap[$sanitizeAddSlashes] = $stringType; + } } return $this->filterTypeMap; @@ -227,24 +243,33 @@ private function getFilterTypeOptions(): array return $this->filterTypeOptions; } - $this->filterTypeOptions = [ - $this->getConstant('FILTER_VALIDATE_INT') => ['min_range', 'max_range'], + $map = [ + 'FILTER_VALIDATE_INT' => ['min_range', 'max_range'], // PHPStan does not yet support FloatRangeType - // $this->getConstant('FILTER_VALIDATE_FLOAT') => ['min_range', 'max_range'], + // 'FILTER_VALIDATE_FLOAT' => ['min_range', 'max_range'], ]; + $this->filterTypeOptions = []; + foreach ($map as $filter => $type) { + $constant = $this->getConstant($filter); + if ($constant === null) { + continue; + } + $this->filterTypeOptions[$constant] = $type; + } + return $this->filterTypeOptions; } /** * @param non-empty-string $constantName */ - private function getConstant(string $constantName): int + private function getConstant(string $constantName): ?int { $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); $valueType = $constant->getValueType(); if (!$valueType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + return null; } return $valueType->getValue(); @@ -301,8 +326,8 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp if ($in instanceof ConstantStringType) { $value = $in->getValue(); - $allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType); - $allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType); + $allowOctal = $this->hasFlag('FILTER_FLAG_ALLOW_OCTAL', $flagsType); + $allowHex = $this->hasFlag('FILTER_FLAG_ALLOW_HEX', $flagsType); if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) { $octalValue = octdec($value); @@ -411,8 +436,16 @@ private function getOptions(Type $flagsType, int $filterValue): array return $options; } - private function hasFlag(int $flag, ?Type $flagsType): bool + /** + * @param non-empty-string $flagName + */ + private function hasFlag(string $flagName, ?Type $flagsType): bool { + $flag = $this->getConstant($flagName); + if ($flag === null) { + return false; + } + if ($flagsType === null) { return false; } @@ -441,9 +474,9 @@ private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsType) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsType) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsType); + return $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType) + || $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType) + || $this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType); } return true;