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