diff --git a/config.xsd b/config.xsd index ab0f940dfa0..06827732e20 100644 --- a/config.xsd +++ b/config.xsd @@ -31,6 +31,7 @@ + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index 6655b184e25..ab9b8703aa1 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -549,6 +549,17 @@ Arrays bigger than this value (100 by default) will be transformed in a generic Please note that changing this setting might introduce unwanted side effects and those side effects won't be considered as bugs. +#### maxIntMaskCombinations +```xml + +``` + +This setting controls the maximum number of integer mask combinations that Psalm will process when analyzing bitwise operations with `TIntMaskVerifier` types. When the number of potential combinations exceeds this limit, Psalm will fall back to using a more generic type to avoid performance issues. + +The default value is 10. Increasing this value may provide more precise type analysis for complex bitwise operations but could negatively impact performance. + #### longScanWarning ```xml max_shaped_array_size = $attribute_text; } + if (isset($config_xml['maxIntMaskCombinations'])) { + $attribute_text = (int)$config_xml['maxIntMaskCombinations']; + $config->max_int_mask_combinations = $attribute_text; + } + if (isset($config_xml['longScanWarning'])) { $attribute_text = (float)$config_xml['longScanWarning']; $config->long_scan_warning = $attribute_text; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index f1b3df52037..eacd8cc1126 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -33,6 +33,7 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TIntMaskVerifier; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TLiteralFloat; @@ -310,6 +311,18 @@ private static function analyzeOperands( bool &$has_string_increment, ?Union &$result_type = null, ): ?Union { + if ($left_type_part instanceof TIntMaskVerifier || $right_type_part instanceof TIntMaskVerifier) { + return TIntMaskVerifierAnalyzer::analyze( + $parent, + $left_type_part, + $right_type_part, + $has_valid_left_operand, + $has_valid_right_operand, + $result_type, + $config->max_int_mask_combinations, + ); + } + if (($left_type_part instanceof TLiteralInt || $left_type_part instanceof TLiteralFloat) && ($right_type_part instanceof TLiteralInt || $right_type_part instanceof TLiteralFloat) && ( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/TIntMaskVerifierAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/TIntMaskVerifierAnalyzer.php new file mode 100644 index 00000000000..5266e3696cf --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/TIntMaskVerifierAnalyzer.php @@ -0,0 +1,504 @@ +potential_ints; + $right_potential_ints = $right_type_part->potential_ints; + $potential_ints = array_merge($left_potential_ints, $right_potential_ints); + $potential_ints = array_unique($potential_ints); + + $result_type = Type::combineUnionTypes( + new Union([new TIntMaskVerifier($potential_ints)]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } + + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TInt) { + if ($right_type_part instanceof TLiteralInt) { + $left_potential_ints = $left_type_part->potential_ints; + + $total_combinations = count($left_potential_ints); + $left_possible_ints = $left_type_part->getPossibleInts(); + + if ($total_combinations <= $max_int_mask_combinations) { + $calculated_masks = []; + foreach ($left_possible_ints as $left_int) { + $result_int = $left_int | $right_type_part->value; + $calculated_masks[] = new TLiteralInt($result_int); + } + + if ($calculated_masks) { + $result_type = Type::combineUnionTypes( + new Union($calculated_masks), + $result_type, + ); + } + } else { + $new_potential_ints = $left_potential_ints; + $new_potential_ints[] = $right_type_part->value; + + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + } + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } else { + $potential_ints = $left_type_part->potential_ints; + $new_verifier = new TIntMaskVerifier($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + return null; + } + } + + if ($left_type_part instanceof TInt && $right_type_part instanceof TIntMaskVerifier) { + if ($left_type_part instanceof TLiteralInt) { + $right_potential_ints = $right_type_part->potential_ints; + + $total_combinations = count($right_potential_ints); + $right_possible_ints = $right_type_part->getPossibleInts(); + + if ($total_combinations <= $max_int_mask_combinations) { + $calculated_masks = []; + foreach ($right_possible_ints as $right_int) { + $result_int = $left_type_part->value | $right_int; + $calculated_masks[] = new TLiteralInt($result_int); + } + + if ($calculated_masks) { + $result_type = Type::combineUnionTypes( + new Union($calculated_masks), + $result_type, + ); + } + } else { + $new_potential_ints = $right_potential_ints; + $new_potential_ints[] = $left_type_part->value; + + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + } + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + + return null; + } else { + $potential_ints = $right_type_part->potential_ints; + $new_verifier = new TIntMaskVerifier($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + return null; + } + } + + return null; + } + + private static function analyzeBitwiseAndOperation( + Atomic $left_type_part, + Atomic $right_type_part, + bool &$has_valid_left_operand, + bool &$has_valid_right_operand, + ?Union &$result_type = null, + int $max_int_mask_combinations = 10, + ): ?Union { + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TIntMaskVerifier) { + $left_potential_ints = $left_type_part->potential_ints; + $right_potential_ints = $right_type_part->potential_ints; + $potential_ints = array_intersect($left_potential_ints, $right_potential_ints); + $potential_ints = array_values($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([new TIntMaskVerifier($potential_ints)]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + + return null; + } + + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TInt) { + if ($right_type_part instanceof TLiteralInt) { + $left_potential_ints = $left_type_part->potential_ints; + + $total_combinations = count($left_potential_ints); + + if ($total_combinations <= $max_int_mask_combinations) { + $left_possible_ints = $left_type_part->getPossibleInts(); + $calculated_masks = []; + foreach ($left_possible_ints as $left_int) { + $result_int = $left_int & $right_type_part->value; + $calculated_masks[] = new TLiteralInt($result_int); + } + if ($calculated_masks) { + $result_type = Type::combineUnionTypes( + new Union($calculated_masks), + $result_type, + ); + } + } else { + $new_potential_ints = $left_potential_ints; + $new_potential_ints[] = $right_type_part->value; + $new_potential_ints = array_unique($new_potential_ints); + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + } + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } else { + $potential_ints = $left_type_part->potential_ints; + $new_verifier = new TIntMaskVerifier($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + return null; + } + } + + if ($left_type_part instanceof TInt && $right_type_part instanceof TIntMaskVerifier) { + if ($left_type_part instanceof TLiteralInt) { + $right_potential_ints = $right_type_part->potential_ints; + + $total_combinations = count($right_potential_ints); + + if ($total_combinations <= $max_int_mask_combinations) { + $right_possible_ints = $right_type_part->getPossibleInts(); + $calculated_masks = []; + foreach ($right_possible_ints as $right_int) { + $result_int = $left_type_part->value & $right_int; + $calculated_masks[] = new TLiteralInt($result_int); + } + if ($calculated_masks) { + $result_type = Type::combineUnionTypes( + new Union($calculated_masks), + $result_type, + ); + } + } else { + $new_potential_ints = $right_potential_ints; + $new_potential_ints[] = $left_type_part->value; + $new_potential_ints = array_unique($new_potential_ints); + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + } + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + + return null; + } else { + $potential_ints = $right_type_part->potential_ints; + $new_verifier = new TIntMaskVerifier($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + return null; + } + } + + return null; + } + + private static function analyzeBitwiseXorOperation( + Atomic $left_type_part, + Atomic $right_type_part, + bool &$has_valid_left_operand, + bool &$has_valid_right_operand, + ?Union &$result_type = null, + ): ?Union { + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TIntMaskVerifier) { + $left_potential_ints = $left_type_part->potential_ints; + $right_potential_ints = $right_type_part->potential_ints; + $potential_ints = array_merge($left_potential_ints, $right_potential_ints); + $potential_ints = array_unique($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([new TIntMaskVerifier($potential_ints)]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + + return null; + } + + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TInt) { + if ($right_type_part instanceof TLiteralInt) { + $left_potential_ints = $left_type_part->potential_ints; + $new_potential_ints = $left_potential_ints; + $new_potential_ints[] = $right_type_part->value; + $new_potential_ints = array_unique($new_potential_ints); + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } else { + $potential_ints = $left_type_part->potential_ints; + $new_verifier = new TIntMaskVerifier($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + return null; + } + } + + if ($left_type_part instanceof TInt && $right_type_part instanceof TIntMaskVerifier) { + if ($left_type_part instanceof TLiteralInt) { + $right_potential_ints = $right_type_part->potential_ints; + $new_potential_ints = $right_potential_ints; + $new_potential_ints[] = $left_type_part->value; + $new_potential_ints = array_unique($new_potential_ints); + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } else { + $potential_ints = $right_type_part->potential_ints; + $new_verifier = new TIntMaskVerifier($potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + } + } + + return null; + } + + private static function analyzeShiftLeftOperation( + Atomic $left_type_part, + Atomic $right_type_part, + bool &$has_valid_left_operand, + bool &$has_valid_right_operand, + ?Union &$result_type = null, + ): ?Union { + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TInt) { + if ($right_type_part instanceof TLiteralInt) { + $left_potential_ints = $left_type_part->potential_ints; + $new_potential_ints = []; + foreach ($left_potential_ints as $int) { + $new_potential_ints[] = $int << $right_type_part->value; + } + $new_potential_ints = array_unique($new_potential_ints); + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } else { + $result_type = Type::combineUnionTypes( + Type::getInt(), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } + } + + if ($left_type_part instanceof TInt && $right_type_part instanceof TIntMaskVerifier) { + $result_type = Type::combineUnionTypes( + Type::getInt(), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } + + return null; + } + + private static function analyzeShiftRightOperation( + Atomic $left_type_part, + Atomic $right_type_part, + bool &$has_valid_left_operand, + bool &$has_valid_right_operand, + ?Union &$result_type = null, + ): ?Union { + if ($left_type_part instanceof TIntMaskVerifier && $right_type_part instanceof TInt) { + if ($right_type_part instanceof TLiteralInt) { + $left_potential_ints = $left_type_part->potential_ints; + $new_potential_ints = []; + foreach ($left_potential_ints as $int) { + $new_potential_ints[] = $int >> $right_type_part->value; + } + $new_potential_ints = array_unique($new_potential_ints); + $new_verifier = new TIntMaskVerifier($new_potential_ints); + $result_type = Type::combineUnionTypes( + new Union([$new_verifier]), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } else { + $result_type = Type::combineUnionTypes( + Type::getInt(), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } + } + + if ($left_type_part instanceof TInt && $right_type_part instanceof TIntMaskVerifier) { + $result_type = Type::combineUnionTypes( + Type::getInt(), + $result_type, + ); + + $has_valid_left_operand = true; + $has_valid_right_operand = true; + return null; + } + + return null; + } +} diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 988c6c10ac0..1a4ca3edd63 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -17,6 +17,7 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TIntMaskVerifier; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; @@ -384,6 +385,25 @@ public static function isContainedBy( return false; } + if ($container_type_part instanceof TIntMaskVerifier) { + if ($input_type_part instanceof TLiteralInt) { + return $container_type_part->isValidValue($input_type_part->value); + } + + if ($input_type_part instanceof TIntMaskVerifier) { + return $container_type_part->isSupersetOf($input_type_part); + } + + if ($input_type_part instanceof TInt) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + } + return true; + } + + return false; + } + if ($input_type_part::class === TFloat::class && $container_type_part instanceof TLiteralFloat) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 045335f3668..8929861f11b 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -12,6 +12,7 @@ use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TIntMaskVerifier; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyOf; use Psalm\Type\Atomic\TKeyedArray; @@ -431,8 +432,24 @@ private static function replaceConditional( $matching_if_types = []; $matching_else_types = []; - $l = $template_type->getAtomicTypes(); - foreach (isset($l['mixed']) ? [$l['mixed']] : $l as $candidate_atomic_type) { + $candidate_types_to_check = []; + $atomic_types = $template_type->getAtomicTypes(); + + $types_to_process = isset($atomic_types['mixed']) ? [$atomic_types['mixed']] : $atomic_types; + + foreach ($types_to_process as $candidate_atomic_type) { + if ($candidate_atomic_type instanceof TIntMaskVerifier) { + // Expand TIntMaskVerifier to all flag literal integer values + $potential_ints = $candidate_atomic_type->potential_ints; + foreach ($potential_ints as $value) { + $candidate_types_to_check[] = new TLiteralInt($value); + } + } else { + $candidate_types_to_check[] = $candidate_atomic_type; + } + } + + foreach ($candidate_types_to_check as $candidate_atomic_type) { $candidate = new Union([$candidate_atomic_type]); if (UnionTypeComparator::isContainedBy( $codebase, diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index f0e711d6807..e9209de4a3c 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -22,6 +22,7 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntMask; use Psalm\Type\Atomic\TIntMaskOf; +use Psalm\Type\Atomic\TIntMaskVerifier; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyOf; use Psalm\Type\Atomic\TKeyedArray; @@ -374,7 +375,7 @@ public static function expandAtomic( $potential_ints[] = $new_value_type->value; } - return TypeParser::getComputedIntsFromMask($potential_ints); + return [new TIntMaskVerifier($potential_ints)]; } if ($return_type instanceof TIntMaskOf) { @@ -408,7 +409,7 @@ public static function expandAtomic( $potential_ints[] = $new_value_type->value; } - return TypeParser::getComputedIntsFromMask($potential_ints); + return [new TIntMaskVerifier($potential_ints)]; } if ($return_type instanceof TConditional) { diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 0d3031288fb..e6dbc2eb972 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -43,6 +43,7 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntMask; use Psalm\Type\Atomic\TIntMaskOf; +use Psalm\Type\Atomic\TIntMaskVerifier; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyOf; @@ -75,11 +76,9 @@ use function array_key_exists; use function array_key_first; use function array_keys; -use function array_map; use function array_merge; use function array_pop; use function array_shift; -use function array_unique; use function array_unshift; use function array_values; use function assert; @@ -534,37 +533,6 @@ private static function getGenericParamClass( throw new LogicException('Should never get here'); } - /** - * @param non-empty-list $potential_ints - * @return non-empty-list - */ - public static function getComputedIntsFromMask(array $potential_ints, bool $from_docblock = false): array - { - /** @var list */ - $potential_values = []; - - foreach ($potential_ints as $ith) { - $new_values = []; - - $new_values[] = $ith; - - if ($ith !== 0) { - foreach ($potential_values as $potential_value) { - $new_values[] = $ith | $potential_value; - } - } - - $potential_values = [...$new_values, ...$potential_values]; - } - - array_unshift($potential_values, 0); - $potential_values = array_unique($potential_values); - - return array_map( - static fn($int): TLiteralInt => new TLiteralInt($int, $from_docblock), - array_values($potential_values), - ); - } /** * @param array> $template_type_map @@ -981,7 +949,7 @@ private static function getTypeFromGenericTree( $potential_ints[] = $atomic_type->value; } - return new Union(self::getComputedIntsFromMask($potential_ints, $from_docblock)); + return new TIntMaskVerifier($potential_ints, $from_docblock); } if ($generic_type_value === 'int-mask-of') { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 96d5e4526ba..d2fd5f044d3 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -23,6 +23,7 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TIntMaskVerifier; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyedArray; @@ -905,7 +906,27 @@ private static function intersectAtomicTypes( $intersection_performed = true; } } - if ($type_1_atomic instanceof TInt && $type_2_atomic instanceof TInt) { + + // Handle TIntMaskVerifier intersection using mask values + if ($type_1_atomic instanceof TIntMaskVerifier && $type_2_atomic instanceof TIntMaskVerifier) { + // Only create intersection if masks are exactly the same + if ($type_1_atomic->mask === $type_2_atomic->mask) { + $intersection_performed = true; + // Create a new verifier with the same mask + // Use the original potential_ints from one of the verifiers as a base + return new TIntMaskVerifier( + $type_1_atomic->potential_ints, + $type_1_atomic->from_docblock || $type_2_atomic->from_docblock, + ); + } else { + // If masks are different, return null as intersection is not possible + $intersection_performed = true; + return null; + } + } + + if ($type_1_atomic instanceof TInt && $type_2_atomic instanceof TInt && + !($type_1_atomic instanceof TIntMaskVerifier || $type_2_atomic instanceof TIntMaskVerifier)) { $int_intersection = TIntRange::intersectIntRanges( TIntRange::convertToIntRange($type_1_atomic), TIntRange::convertToIntRange($type_2_atomic), diff --git a/src/Psalm/Type/Atomic/TIntMaskVerifier.php b/src/Psalm/Type/Atomic/TIntMaskVerifier.php new file mode 100644 index 00000000000..d9136655dec --- /dev/null +++ b/src/Psalm/Type/Atomic/TIntMaskVerifier.php @@ -0,0 +1,105 @@ + $potential_ints + */ + public function __construct( + public array $potential_ints, + bool $from_docblock = false, + ) { + parent::__construct($from_docblock); + + if (!in_array(0, $this->potential_ints)) { + array_unshift($this->potential_ints, 0); + } + + $this->mask = 0; + foreach ($this->potential_ints as $int) { + $this->mask |= $int; + } + } + + + #[Override] + public function getKey(bool $include_extra = true): string + { + return 'int-mask-verifier<' . implode(',', $this->potential_ints) . '>'; + } + + #[Override] + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool + { + return false; + } + + /** + * Checks if the given integer is a valid value based on the mask. + * + * @param int $i The integer to check. + * @return bool True if the integer is valid, false otherwise. + */ + public function isValidValue(int $i): bool + { + if ($i === 0) { + return true; + } + + return ($this->mask & $i) === $i; + } + + /** + * Checks if this verifier is a superset of the given verifier. + * + * @param TIntMaskVerifier $input_type_part The verifier to check against. + * @return bool True if this verifier is a superset of the other, false otherwise. + */ + public function isSupersetOf(TIntMaskVerifier $input_type_part): bool + { + return ($this->mask & $input_type_part->mask) === $input_type_part->mask; + } + + /** + * Compute all possible concrete ints that can be formed by OR-ing any subset of potential_ints. + * Note: This may grow exponentially with the number of elements; callers should guard usage. + * + * @return array Sorted unique list of possible ints. + */ + public function getPossibleInts(): array + { + $values = [0]; + $bits = $this->potential_ints; + + foreach ($bits as $bit) { + $new = []; + foreach ($values as $v) { + $new[] = $v | $bit; + } + $values = array_unique(array_merge($values, $new)); + } + + sort($values); + return $values; + } +} diff --git a/tests/TIntMaskVerifierTest.php b/tests/TIntMaskVerifierTest.php new file mode 100644 index 00000000000..2fc89368a52 --- /dev/null +++ b/tests/TIntMaskVerifierTest.php @@ -0,0 +1,345 @@ +assertTrue($verifier->isValidValue(0)); + } + + public function testIsValidValueWithValidCombinations(): void + { + $verifier = new TIntMaskVerifier([1, 2, 4]); + + // Test individual bits + $this->assertTrue($verifier->isValidValue(1)); + $this->assertTrue($verifier->isValidValue(2)); + $this->assertTrue($verifier->isValidValue(4)); + + // Test combinations + $this->assertTrue($verifier->isValidValue(3)); // 1 | 2 + $this->assertTrue($verifier->isValidValue(5)); // 1 | 4 + $this->assertTrue($verifier->isValidValue(6)); // 2 | 4 + $this->assertTrue($verifier->isValidValue(7)); // 1 | 2 | 4 + } + + public function testIsValidValueWithInvalidValue(): void + { + $verifier = new TIntMaskVerifier([1, 2, 4]); + + // Test invalid values + $this->assertFalse($verifier->isValidValue(8)); // Not in mask + $this->assertFalse($verifier->isValidValue(9)); // 1 | 8, but 8 not in mask + $this->assertFalse($verifier->isValidValue(16)); // Not in mask + } + + public function testIsSupersetOf(): void + { + $superset = new TIntMaskVerifier([1, 2, 4, 8]); + $subset = new TIntMaskVerifier([1, 2]); + + $this->assertTrue($superset->isSupersetOf($subset)); + $this->assertFalse($subset->isSupersetOf($superset)); + } + + public function testIsSupersetOfSameSet(): void + { + $verifier1 = new TIntMaskVerifier([1, 2, 4]); + $verifier2 = new TIntMaskVerifier([1, 2, 4]); + + $this->assertTrue($verifier1->isSupersetOf($verifier2)); + $this->assertTrue($verifier2->isSupersetOf($verifier1)); + } + + public function testGetPossibleInts(): void + { + $verifier = new TIntMaskVerifier([1, 2]); + $possibleInts = $verifier->getPossibleInts(); + + $expected = [0, 1, 2, 3]; // All combinations of 1|2 + $this->assertSame($expected, $possibleInts); + } + + public function testGetPossibleIntsWithThreeBits(): void + { + $verifier = new TIntMaskVerifier([1, 2, 4]); + $possibleInts = $verifier->getPossibleInts(); + + $expected = [0, 1, 2, 3, 4, 5, 6, 7]; // All combinations + $this->assertSame($expected, $possibleInts); + } + + public function testMaskCalculation(): void + { + $verifier = new TIntMaskVerifier([1, 2, 4]); + $this->assertSame(7, $verifier->mask); // 1 | 2 | 4 = 7 + } + + #[Override] + public function providerValidCodeParse(): iterable + { + return [ + 'intMaskBasicUsage' => [ + 'code' => ' $flags + * @return int + */ + function processFlags(int $flags): int { + return $flags; + } + + processFlags(0); + processFlags(1); + processFlags(3); // 1 | 2 + processFlags(7); // 1 | 2 | 4', + ], + 'intMaskCombination' => [ + 'code' => ' $flags1 + * @param int-mask<4, 8> $flags2 + * @return int-mask<1, 2, 4, 8> + */ + function combineFlags(int $flags1, int $flags2): int { + return $flags1 | $flags2; + }', + ], + 'intMaskIntersection' => [ + 'code' => ' $flags1 + * @param int-mask<2, 4, 8> $flags2 + * @return int-mask<2, 4> + */ + function getCommonFlags(int $flags1, int $flags2): int { + return $flags1 & $flags2; + }', + ], + 'intMaskZeroValue' => [ + 'code' => ' $flags + */ + function isNoFlags(int $flags): bool { + return $flags === 0; + } + + isNoFlags(0);', + ], + 'intMaskWithClassConstants' => [ + 'code' => ' $perms + */ + public static function check(int $perms): void { + if ($perms & self::READ) { + echo "Can read"; + } + } + } + + Permissions::check(Permissions::READ | Permissions::WRITE);', + ], + 'intMaskReturnsSpecificType' => [ + 'code' => ' + */ + function getFlags(): int { + return 1 | 2; + } + + $flags = getFlags();', + 'assertions' => [ + '$flags===' => 'int-mask-verifier<0,1,2,4>', + ], + ], + 'intMaskBitwiseAND' => [ + 'code' => ' $flags1 + * @param int-mask<2, 4, 8, 16> $flags2 + * @return int-mask<2, 4, 8> + */ + function andFlags(int $flags1, int $flags2): int { + return $flags1 & $flags2; + } + + $result = andFlags(7, 6);', + 'assertions' => [ + '$result===' => 'int-mask-verifier<0,2,4,8>', + ], + ], + 'intMaskBitwiseOR' => [ + 'code' => ' $flags1 + * @param int-mask<4, 8> $flags2 + * @return int-mask<1, 2, 4, 8> + */ + function orFlags(int $flags1, int $flags2): int { + return $flags1 | $flags2; + } + + $result = orFlags(3, 12); // (1|2) | (4|8) = 15', + 'assertions' => [ + '$result===' => 'int-mask-verifier<0,1,2,4,8>', + ], + ], + 'intMaskBitwiseXOR' => [ + 'code' => ' $flags1 + * @param int-mask<2, 4, 8, 16> $flags2 + * @return int-mask<1, 2, 4, 8, 16> + */ + function xorFlags(int $flags1, int $flags2): int { + return $flags1 ^ $flags2; + } + + $result = xorFlags(7, 6); // (1|2|4) ^ (2|4) = 1', + 'assertions' => [ + '$result===' => 'int-mask-verifier<0,1,2,4,8,16>', + ], + ], + 'intMaskShiftLeft' => [ + 'code' => ' $flags + * @return int-mask<2, 4, 8> + */ + function shiftLeft(int $flags): int { + return $flags << 1; + } + + $result = shiftLeft(3); // (1|2) << 1 = 6', + 'assertions' => [ + '$result===' => 'int-mask-verifier<0,2,4,8>', + ], + ], + 'intMaskShiftRight' => [ + 'code' => ' $flags + * @return int-mask<1, 2, 4> + * */ + function shiftRight(int $flags): int { + return $flags >> 1; + } + $result = shiftRight(12); // (4|8) >> 1 = 6', + 'assertions' => [ + '$result===' => 'int-mask-verifier<0,1,2,4>', + ], + ], + 'intMaskSameMaskBitwiseOperations' => [ + 'code' => ' $flags1 */ + $flags1 = 0; + /** @var int-mask<1, 2, 4> $flags2 */ + $flags2 = 0; + /** @var int-mask<2, 4, 8> $flags3 */ + $flags3 = 0; + + $and = $flags1 & $flags2; // int-mask<1, 2, 4> + $or = $flags1 | $flags2; // int-mask<1, 2, 4> + $xor = $flags1 ^ $flags2; // int-mask<1, 2, 4> + $shiftLeft = $flags1 << 1; // int-mask<2, 4, 8> + $shiftRight = $flags3 >> 1; // int-mask<1, 2, 4>', + 'assertions' => [ + '$and===' => 'int-mask-verifier<0,1,2,4>', + '$or===' => 'int-mask-verifier<0,1,2,4>', + '$xor===' => 'int-mask-verifier<0,1,2,4>', + '$shiftLeft===' => 'int-mask-verifier<0,2,4,8>', + '$shiftRight===' => 'int-mask-verifier<0,1,2,4>', + ], + ], + 'intMaskComplexBitwiseExpressions' => [ + 'code' => ' $maskA */ + $maskA = 0; + /** @var int-mask<8, 16> $maskB */ + $maskB = 0; + /** @var int-mask<4, 8, 32> $maskC */ + $maskC = 0; + + $orAnd = ($maskA | $maskB) & $maskC; + $xorOrAnd = ($maskA ^ $maskB) | ($maskC & $maskA); + $shiftLeftOr = ($maskA << 3) | $maskB; // (int-mask<8, 16, 32>) | int-mask<8, 16> + $orShiftRight = ($maskA | $maskB) >> 2; // int-mask<1, 2, 4, 8, 16> >> 2', + 'assertions' => [ + '$orAnd===' => 'int-mask-verifier<0,4,8>', + '$xorOrAnd===' => 'int-mask-verifier<0,1,2,4,8,16>', + '$shiftLeftOr===' => 'int-mask-verifier<0,8,16,32>', + '$orShiftRight===' => 'int-mask-verifier<0,1,2,4>', + ], + ], + ]; + } + + #[Override] + public function providerInvalidCodeParse(): iterable + { + return [ + 'intMaskInvalidValue' => [ + 'code' => ' $flags + */ + function processFlags(int $flags): void {} + + processFlags(8); // Invalid: 8 is not in mask', + 'error_message' => 'InvalidArgument', + ], + 'intMaskInvalidCombination' => [ + 'code' => ' $flags + */ + function processFlags(int $flags): void {} + + processFlags(9); // Invalid: 9 = 1|8, but 8 is not in mask', + 'error_message' => 'InvalidArgument', + ], + 'intMaskNegativeValue' => [ + 'code' => ' $flags + */ + function processFlags(int $flags): void {} + + processFlags(-1); // Invalid: negative values not allowed', + 'error_message' => 'InvalidArgument', + ], + 'intMaskReturnTypeViolation' => [ + 'code' => ' + */ + function getFlags(): int { + return 16; // Invalid: 16 is not in mask + }', + 'error_message' => 'InvalidReturnStatement', + ], + ]; + } +} diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 9f231b55f65..4ed60e16475 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -1081,7 +1081,7 @@ public function testLiteralIntUnionWithSeparators(): void public function testIntMaskWithIntsWithSeparators(): void { - $this->assertSame('0|10|20|30', Type::parseString('int-mask<1_0, 2_0>')->getId()); + $this->assertSame('int-mask-verifier<0,10,20>', Type::parseString('int-mask<1_0, 2_0>')->getId()); } public function testSingleLiteralFloat(): void @@ -1110,19 +1110,19 @@ public function testIntMaskWithInts(): void { $docblock_type = Type::parseString('int-mask<0, 1, 2, 4>'); - $this->assertSame('0|1|2|3|4|5|6|7', $docblock_type->getId()); + $this->assertSame('int-mask-verifier<0,1,2,4>', $docblock_type->getId()); $docblock_type = Type::parseString('int-mask<1, 2, 4>'); - $this->assertSame('0|1|2|3|4|5|6|7', $docblock_type->getId()); + $this->assertSame('int-mask-verifier<0,1,2,4>', $docblock_type->getId()); $docblock_type = Type::parseString('int-mask<1, 4>'); - $this->assertSame('0|1|4|5', $docblock_type->getId()); + $this->assertSame('int-mask-verifier<0,1,4>', $docblock_type->getId()); $docblock_type = Type::parseString('int-mask'); - $this->assertSame('0|1|256|257|512|513|768|769', $docblock_type->getId()); + $this->assertSame('int-mask-verifier<0,1,256,512>', $docblock_type->getId()); } public function testIntMaskWithClassConstant(): void