expectException(CodeException::class); // $this->expectExceptionMessage("PossiblyUndefinedIntArrayOffset"); // $this->testConfig->ensure_array_int_offsets_exist = true; // $file_path = getcwd() . '/src/somefile.php'; // $this->addFile( // $file_path, // ' */ // public const CONST = [1, 2, 3]; // /** // * @param key-of $key // */ // public function bar(int $key): int // { // return static::CONST[$key]; // } // } // ' // ); // $this->analyzeFile($file_path, new Context()); // } public function testUseObjectConstant(): void { $file1 = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file1.php'; $file2 = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file2.php'; $this->addFile( $file1, 'addFile( $file2, 'analyzeFile($file1, new Context()); $this->analyzeFile($file2, new Context()); } public function providerValidCodeParse(): iterable { return [ 'constantInFunction' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$a' => 'int', '$b' => 'string', ], ], 'getClassConstantValue' => [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => ['MixedArgument'], ], 'undefinedConstant' => [ 'code' => ' [], 'ignored_issues' => ['UndefinedConstant'], ], 'suppressUndefinedClassConstant' => [ 'code' => ' [], 'ignored_issues' => ['MixedAssignment'], ], 'hardToDefineClassConstant' => [ 'code' => ' 4, "name" => 3 ]; const B = 4; } echo A::C[4];', ], 'sameNamedConstInOtherClass' => [ 'code' => ' "one", ]; } echo A::C[4];', ], 'onlyMatchingConstantOffset' => [ 'code' => ' 1, "two" => 2 ]; } foreach (A::KEYS as $key) { if (isset(A::ARR[$key])) { echo A::ARR[$key]; } }', ], 'stringArrayOffset' => [ 'code' => ' 1, "b" => 2, ]; } function foo(string $s) : void { if (!isset(A::C[$s])) { return; } if ($s === "Hello") {} }', ], 'noExceptionsOnMixedArrayKey' => [ 'code' => ' A::class, "type2" => B::class, ]; public function bar(array $data): void { if (!isset(self::TYPES[$data["type"]])) { throw new \InvalidArgumentException("Unknown type"); } $class = self::TYPES[$data["type"]]; $ret = finder($data["id"]); if (!$ret || !$ret instanceof $class) { throw new \InvalidArgumentException; } } }', 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedArrayOffset', 'MixedAssignment'], ], 'lateConstantResolution' => [ 'code' => ' [ '$a' => 'string', '$b' => 'string', ], ], 'lateConstantResolutionParentArrayPlus' => [ 'code' => ' true]; } class B extends A { /** @var array{a: true, b: true, ...} */ public const ARR = parent::ARR + ["b" => true]; } class C extends B { public const ARR = parent::ARR + ["c" => true]; } /** @param array{a: true, b: true, c: true} $arg */ function foo(array $arg): void {} foo(C::ARR); ', ], 'lateConstantResolutionParentArraySpread' => [ 'code' => ' [ 'code' => ' [ 'code' => ' $arg */ function foo(array $arg): void {} foo(C::ARR); ', ], 'classConstConcatEol' => [ 'code' => ' ['$foo' => 'string'], ], 'dynamicClassConstFetch' => [ 'code' => ' ['$_trace===' => "'bar'"], ], 'unsafeInferenceClassConstFetch' => [ 'code' => ' ['$_trace' => 'mixed'], ], 'FinalInferenceClassConstFetch' => [ 'code' => ' ['$_trace===' => "'bar'"], ], 'dynamicClassConstFetchClassString' => [ 'code' => ' ['$d===' => '1'], ], 'allowConstCheckForDifferentPlatforms' => [ 'code' => ' [ 'code' => ' [ 'code' => ' 1, B::class => 2, ]; /** * @param class-string $s */ function foo(string $s) : void { if (isset(C[$s])) {} }', ], 'resolveClassConstToCurrentClass' => [ 'code' => ' [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'resolveCalculatedConstant' => [ 'code' => ' [], 'ignored_issues' => ['MixedArgument'], ], 'arrayAccessAfterIsset' => [ 'code' => ' ["c" => false], "c" => ["c" => true], "d" => ["c" => true] ]; } /** @var string */ $s = "b"; if (isset(C::A[$s]["c"]) && C::A[$s]["c"] === false) {}', ], 'namespacedConstantInsideClosure' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' */ function getMap(): array { return Mapper::MAP; } class Mapper { public const MAP = [ Foo::class => self::A, Foo::BAR => self::A, ]; private const A = 5; } class Foo { public const BAR = "bar"; }', ], 'resolveConstArrayAsList' => [ 'code' => ' $value */ function test($value): void { print_r($value); } test(Test1::VALUES); test(Test2::VALUES);', ], 'resolveConstantFetchViaFunction' => [ 'code' => ' [ 'code' => ' null, "A01" => null, "A02" => null, "A03" => null, "A04" => null, "A05" => null, "A06" => null, "A07" => null, "A010" => null, "A011" => null, "A012" => null, "A013" => null, "A014" => null, "A015" => null, "A016" => null, "A017" => null, "A020" => null, "A021" => null, "A022" => null, "A023" => null, "A024" => null, "A025" => null, "A026" => null, "A027" => null, "A030" => null, "A031" => null, "A032" => null, "A033" => null, "A034" => null, "A035" => null, "A036" => null, "A037" => null, "A040" => null, "A041" => null, "A042" => null, "A043" => null, "A044" => null, "A045" => null, "A046" => null, "A047" => null, "A050" => null, "A051" => null, "A052" => null, "A053" => null, "A054" => null, "A055" => null, "A056" => null, "A057" => null, "A060" => null, "A061" => null, "A062" => null, "A063" => null, "A064" => self::SUCCEED, "A065" => self::FAIL, ]; const SUCCEED = "SUCCEED"; const FAIL = "FAIL"; /** * @param string $code */ public static function will_succeed($code) : bool { // False positive TypeDoesNotContainType - string(SUCCEED) cannot be identical to null // This seems to happen because the array has a lot of entries. return (self::LOOKUP[strtoupper($code)] ?? null) === self::SUCCEED; } }', ], 'keyOf' => [ 'code' => ' "a", 2 => "b", 3 => "c" ]; /** * @param key-of $i */ public static function foo(int $i) : void {} } A::foo(1); A::foo(2); A::foo(3);', ], 'valueOf' => [ 'code' => ' "a", 2 => "b", 3 => "c" ]; /** * @param value-of $j */ public static function bar(string $j) : void {} } A::bar("a"); A::bar("b"); A::bar("c");', ], 'valueOfDefault' => [ 'code' => ' "a", 2 => "b", 3 => "c" ]; /** * @var value-of */ public $foo = "a"; }', ], 'wildcardEnum' => [ 'code' => ' [ 'code' => ' [ 'code' => ' */ class A implements AInterface { const C_1 = 1; const C_2 = 2; const C_3 = 3; const D_4 = 4; public function foo($i) { return $i; } } $a = new A(); $a->foo(1); $a->foo(2); $a->foo(3); $a->foo(A::D_4);', ], 'wildcardVarAndReturn' => [ 'code' => 'number = $number; } /** * @return Numbers::* */ public function get(): int { return $this->number; } }', ], 'lowercaseStringAccessClassConstant' => [ 'code' => ' 1, "b" => 2, "c" => 3 ]; } /** * @param lowercase-string $s */ function foo(string $s, string $t) : void { echo A::C[$t]; echo A::C[$s]; }', ], 'getClassConstantOffset' => [ 'code' => ' "string" ]; private const B = self::A[0]; public function foo(): string { return self::B; } }', ], 'bitwiseOrClassConstant' => [ 'code' => ' [ '$c' => 'int', ], ], 'bitwiseAndClassConstant' => [ 'code' => ' [ '$c' => 'int', ], ], 'bitwiseXorClassConstant' => [ 'code' => ' [ '$c' => 'int', ], ], 'bitwiseNotClassConstant' => [ 'code' => ' [ '$a' => 'int', '$b' => 'string', ], ], 'booleanNotClassConstant' => [ 'code' => ' [ '$a' => 'false', '$b' => 'true', ], ], 'protectedClassConstantAccessibilitySameNameInChild' => [ 'code' => ' */ protected const A = 1; public static function test(): void { echo B::A; } } class B extends A { protected const A = 2; } A::test();', ], 'referenceClassConstantWithSelf' => [ 'code' => ' */ public const KEYS = []; /** @var array */ public const VALUES = []; } class B extends A { public const VALUES = [\'there\' => self::KEYS[\'hi\']]; public const KEYS = [\'hi\' => CONSTANTS::THERE]; } class CONSTANTS { public const THERE = \'there\'; } echo B::VALUES["there"];', ], 'internalConstWildcard' => [ 'code' => ' [ 'code' => 'level = $level; } /** * @psalm-return self */ public static function readUncommitted(): self { return new self(self::READ_UNCOMMITTED); } /** * @psalm-return self */ public static function readCommitted(): self { return new self(self::READ_COMMITTED); } /** * @psalm-return self */ public static function repeatableRead(): self { return new self(self::REPEATABLE_READ); } /** * @psalm-return self */ public static function serializable(): self { return new self(self::SERIALIZABLE); } /** * @psalm-return T */ public function toString(): string { return $this->level; } }', ], 'dirAndFileInConstInitializersAreNonEmptyString' => [ 'code' => ' [ '$dir===' => 'non-empty-string', '$file===' => 'non-empty-string', ], ], 'lineInConstInitializersIsInt' => [ 'code' => ' [ '$line' => 'int', ], ], 'classMethodTraitAndFunctionInConstInitializersAreStrings' => [ 'code' => ' [ '$cls' => 'string', '$mtd' => 'string', '$trt' => 'string', '$fcn' => 'string', ], ], 'concatWithMagicInConstInitializersIsNoEmptyString' => [ 'code' => ' [ '$dir===' => 'non-empty-string', '$file===' => 'non-empty-string', ], ], 'noCrashWithStaticInDocblock' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$arr===' => 'list{1, 2}', ], ], 'keysInUnpackedArrayAreReset' => [ 'code' => ' 2]]; } $arr = C::A; ', 'assertions' => [ '$arr===' => 'list{2}', ], ], 'arrayKeysSequenceContinuesAfterExplicitIntKey' => [ 'code' => ' "a", "z", 10 => "aa", "zz"]; } $arr = C::A; ', 'assertions' => [ '$arr===' => "array{10: 'aa', 11: 'zz', 5: 'a', 6: 'z'}", ], ], 'arrayKeysSequenceContinuesAfterNonIntKey' => [ 'code' => ' "a", "zz" => "z", "aa"]; } $arr = C::A; ', 'assertions' => [ '$arr===' => "array{5: 'a', 6: 'aa', zz: 'z'}", ], ], 'unresolvedConstWithUnaryMinus' => [ 'code' => ' -1, K => 6, ]; /** * @param int $k */ public static function f(int $k): void { $a = self::M; print_r($a); } }', ], 'classConstantReferencingEnumCase' => [ 'code' => ' [ '$c===' => 'enum(E::Z)', ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'classConstWithParamOut' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $arg */ function foo(array $arg): void {} foo([...A::ARR]); ', ], 'classConstCovariant' => [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => [], 'php_version' => '8.1', ], 'inheritedConstDoesNotOverride' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$a===' => "'baz'", ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'finalConstInterface' => [ 'code' => ' [ '$a===' => "'baz'", ], 'ignored_issues' => [], 'php_version' => '8.1', ], 'constantTypeRespectsLiteralStringLimit' => [ 'code' => <<<'PHP' A::T, 'b' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ]; } $z = B::ARRAY['b']; PHP, ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'constantDefinedInFunctionButNotCalled' => [ 'code' => ' 'UndefinedConstant', ], 'undefinedClassConstantInParamDefault' => [ 'code' => ' 'UndefinedConstant', ], 'nonMatchingConstantOffset' => [ 'code' => ' 1, "two" => 2 ]; const ARR2 = [ "three" => 3, "four" => 4 ]; } foreach (A::KEYS as $key) { if (isset(A::ARR[$key])) { echo A::ARR2[$key]; } }', 'error_message' => 'InvalidArrayOffset', ], 'objectLikeConstArrays' => [ 'code' => ' "zero", self::B => "two", ]; } if (C::ARR[C::A] === "two") {}', 'error_message' => 'TypeDoesNotContainType', ], 'missingClassConstInArray' => [ 'code' => ' 'UndefinedConstant', ], 'resolveConstToCurrentClassWithBadReturn' => [ 'code' => ' 'InvalidReturnStatement', 'ignored_issues' => [], 'php_version' => '8.1', ], 'outOfScopeDefinedConstant' => [ 'code' => ' 'UndefinedConstant', ], 'preventStaticClassConstWithoutRef' => [ 'code' => ' 'UndefinedConstant', ], 'noCyclicConstReferences' => [ 'code' => ' 'CircularReference', ], 'keyOfBadValue' => [ 'code' => ' "a", 2 => "b", 3 => "c" ]; /** * @param key-of $i */ public static function foo(int $i) : void {} } A::foo(4);', 'error_message' => 'InvalidArgument', ], 'valueOfBadValue' => [ 'code' => ' "a", 2 => "b", 3 => "c" ]; /** * @param value-of $j */ public static function bar(string $j) : void {} } A::bar("d");', 'error_message' => 'InvalidArgument', ], 'wildcardEnumBadValue' => [ 'code' => ' 'InvalidArgument', ], 'wildcardEnumAnyTemplateExtendConstantBadValue' => [ 'code' => ' */ class A implements AInterface { const C_1 = 1; const C_2 = 2; const C_3 = 3; const D_4 = 4; public function foo($i) { return $i; } } $a = new A(); $a->foo(5); ', 'error_message' => 'InvalidArgument', ], 'correctMessage' => [ 'code' => ' "a", 2 => "b"][$s]; }', 'error_message' => "offset value of '1|0", ], 'constantWithMissingClass' => [ 'code' => ' 'UndefinedClass', ], 'duplicateConstants' => [ 'code' => ' 'DuplicateConstant', ], 'constantDuplicatesEnumCase' => [ 'code' => ' 'DuplicateConstant', 'ignored_issues' => [], 'php_version' => '8.1', ], 'enumCaseDuplicatesConstant' => [ 'code' => ' 'DuplicateConstant', 'ignored_issues' => [], 'php_version' => '8.1', ], 'returnValueofNonExistantConstant' => [ 'code' => ' */ public function bar(): string { return self::BAR[0]; } } ', 'error_message' => 'UnresolvableConstant', ], 'returnValueofStaticConstant' => [ 'code' => ' */ public function bar(): string { return static::BAR[0]; } } ', 'error_message' => 'UnresolvableConstant', ], 'takeKeyofNonExistantConstant' => [ 'code' => ' $key */ public function bar(int $key): string { return static::BAR[$key]; } } ', 'error_message' => 'UnresolvableConstant', ], 'takeKeyofStaticConstant' => [ 'code' => ' $key */ public function bar(int $key): string { return static::BAR[$key]; } } ', 'error_message' => 'UnresolvableConstant', ], 'invalidConstantAssignmentType' => [ 'code' => ' "InvalidConstantAssignmentValue", ], 'invalidConstantAssignmentTypeResolvedLate' => [ 'code' => ' "InvalidConstantAssignmentValue", ], 'classConstContravariant' => [ 'code' => ' "LessSpecificClassConstantType", ], 'classConstAmbiguousInherit' => [ 'code' => ' 'AmbiguousConstantInheritance', ], 'overrideClassConstFromInterface' => [ 'code' => ' 'OverriddenInterfaceConstant', ], 'overrideClassConstFromInterfaceWithInterface' => [ 'code' => ' 'OverriddenInterfaceConstant', ], 'overrideClassConstFromInterfaceWithExtraIrrelevantInterface' => [ 'code' => ' "InvalidClassConstantType", 'ignored_issues' => [], 'php_version' => '8.1', ], 'overrideFinalClassConstFromExtendedClass' => [ 'code' => ' "OverriddenFinalConstant", 'ignored_issues' => [], 'php_version' => '8.1', ], 'overrideFinalClassConstFromImplementedInterface' => [ 'code' => ' "OverriddenFinalConstant", 'ignored_issues' => [], 'php_version' => '8.1', ], 'finalConstantIsIllegalBefore8.1' => [ 'code' => ' 'ParseError - src' . DIRECTORY_SEPARATOR . 'somefile.php:5:44', 'ignored_issues' => [], 'php_version' => '8.0', ], 'classStringIsRequiredToAccessClassConstant' => [ 'code' => ' 'InvalidStringClass', ], ]; } }