allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'offsetGet("a"); takesString($s); foreach ($i as $s2) { takesString($s2); } }' ); $this->analyzeFile('somefile.php', new Context()); } public function testPhpStormGenericsWithValidTraversableArgument(): void { Config::getInstance()->allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testPhpStormGenericsWithClassProperty(): void { Config::getInstance()->allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'bar; } }' ); $this->analyzeFile('somefile.php', new Context()); } public function testPhpStormGenericsWithGeneratorArray(): void { Config::getInstance()->allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testPhpStormGenericsWithValidIterableArgument(): void { Config::getInstance()->allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testPhpStormGenericsInvalidArgument(): void { $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('InvalidScalarArgument'); Config::getInstance()->allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'offsetGet("a"); takesInt($s); }' ); $this->analyzeFile('somefile.php', new Context()); } public function testPhpStormGenericsNoTypehint(): void { $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('PossiblyInvalidMethodCall'); Config::getInstance()->allow_phpstorm_generics = true; $this->addFile( 'somefile.php', 'offsetGet("a"); }' ); $this->analyzeFile('somefile.php', new Context()); } public function testInvalidParamDefault(): void { $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('InvalidParamDefault'); $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testInvalidParamDefaultButAllowedInConfig(): void { Config::getInstance()->add_param_default_to_docblock_type = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testInvalidTypehintParamDefaultButAllowedInConfig(): void { $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessage('InvalidParamDefault'); Config::getInstance()->add_param_default_to_docblock_type = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } /** * @return iterable,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'nopType' => [ ' [ '$a' => 'int', ], ], 'validDocblockReturn' => [ ' */ function foo2(): array { return ["hello"]; } /** * @return array */ function foo3(): array { return ["hello"]; }', ], 'reassertWithIs' => [ ' [], 'error_level' => ['RedundantConditionGivenDocblockType'], ], 'checkArrayWithIs' => [ ' [], 'error_level' => ['RedundantConditionGivenDocblockType'], ], 'goodDocblock' => [ ' [ ' [ 'foo(); $a->bar = 7; takeA($a);', ], 'invalidDocblockParamSuppress' => [ ' [ ' [ ' */ $a = []; $a[0]->getMessage();', ], 'mixedDocblockParamTypeDefinedInParent' => [ ' [ ' [ 'foo(); } }', ], 'psalmVar' => [ ' */ public $foo = []; public function updateFoo(): void { $this->foo[5] = "hello"; } }', ], 'psalmParam' => [ ' $a * @param string[] $a */ function foo(array $a): void { foreach ($a as $key => $value) { takesInt($key); } }', ], 'returnDocblock' => [ ' [ ' new stdClass, "goodbye" => new stdClass]; } $a = null; $b = null; /** * @var string $key * @var stdClass $value */ foreach (foo() as $key => $value) { $a = $key; $b = $value; }', 'assertions' => [ '$a' => 'null|string', '$b' => 'null|stdClass', ], ], 'allowOptionalParamsToBeEmptyArray' => [ ' [ ' [ ' [ '|null), b?:Closure():array, c?:Closure():array, d?:Closure():array, e?:Closure():(array{f:null|string, g:null|string, h:null|string, i:string, j:mixed, k:mixed, l:mixed, m:mixed, n:bool, o?:array{0:string}}|null), p?:Closure():(array{f:null|string, g:null|string, h:null|string, q:string, i:string, j:mixed, k:mixed, l:mixed, m:mixed, n:bool, o?:array{0:string}}|null), r?:Closure():(array|null), s:array} */ $arr = []; $arr["a"]();', ], 'megaClosureAnnotationWithSpacing' => [ '|null), * b?: Closure() : array, * c?: Closure() : array, * d?: Closure() : array, * e?: Closure() : (array{ * f: null|string, * g: null|string, * h: null|string, * i: string, * j: mixed, * k: mixed, * l: mixed, * m: mixed, * n: bool, * o?: array{0:string} * }|null), * p?: Closure() : (array{ * f: null|string, * g: null|string, * h: null|string, * q: string, * i: string, * j: mixed, * k: mixed, * l: mixed, * m: mixed, * n: bool, * o?: array{0:string} * }|null), * r?: Closure() : (array|null), * s: array * } * * Some text */ $arr = []; $arr["a"]();', ], 'multipeLineGenericArray' => [ ', * array * > * * @psalm-type RuleArray = array{ * rule: string, * controller?: class-string<\Exception>, * redirect?: string, * code?: int, * type?: string, * middleware?: MiddlewareArray * } * * Foo Bar */ class A {}', ], 'builtInClassInAShape' => [ ' [ ' [ ' [ ' [ ' [ '$a' => 'string', '$b' => 'string', '$c' => 'string', ], ], 'valueReturnType' => [ ' [ ' [ ' [ ' [ ' [ ' */ class Bar { public function foo() : void { $bar = /** @return TA */ function() { return ["hello"]; }; /** @var array */ $bat = [$bar(), $bar()]; foreach ($bat as $b) { echo $b[0]; } } } /** * @psalm-type _A=array{elt:int} * @param _A $p * @return _A */ function f($p) { /** @var _A */ $r = $p; return $r; }', ], 'listUnpackWithDocblock' => [ 'bar();', ], 'spaceInType' => [ ' [ ' \Psalm\Config::REPORT_INFO, 'MissingReturnType' => \Psalm\Config::REPORT_INFO, ], ], 'objectWithPropertiesAnnotation' => [ 'foo; } $s = new \stdClass(); $s->foo = "hello"; foo($s); class A { /** @var string */ public $foo = "hello"; } foo(new A);', ], 'refineTypeInNestedCall' => [ ' $arr */ foreach (array_filter(array_keys($arr), function (string $key) : bool { return strpos($key, "BAR") === 0; }) as $envVar) { yield $envVar => [getenv($envVar)]; } }', ], 'allowAnnotationOnServer' => [ ' $_SERVER */ foreach (array_filter(array_keys($_SERVER), function (string $key) : bool { return strpos($key, "BAR") === 0; }) as $envVar) { yield $envVar => [getenv($envVar)]; } }', ], 'annotationOnForeachItems' => [ ' $_) {} if (is_null($item)) {} } function bat(array $arr) : void { $item = null; /** * @psalm-suppress MixedArrayAccess * @var string $item */ foreach ($arr as list($item)) {} if (is_null($item)) {} } function baz(array $arr) : void { $item = null; /** * @psalm-suppress MixedArrayAccess * @var string $item */ foreach ($arr as list($item => $_)) {} if (is_null($item)) {} }', [], [ 'MixedAssignment', ], ], 'extraneousDocblockParamName' => [ ' [ ' $arr */ function foo(array $arr) : void { foreach ($arr as $a) {} echo $a; } foo(["a", "b", "c"]); /** @param array $arr */ function bar(array $arr) : void { if (!$arr) { return; } foo($arr); }', ], 'nonEmptyArrayInNamespace' => [ ' $arr */ function foo(array $arr) : void { foreach ($arr as $a) {} echo $a; } foo(["a", "b", "c"]); /** @param array $arr */ function bar(array $arr) : void { if (!$arr) { return; } foo($arr); }', ], 'noExceptionOnIntersection' => [ ' [ 'foo(); $a->bar(); }', ], 'allowClosingComma' => [ ' "", "bar" => "", "baz" => ""];', ], 'returnNumber' => [ ' [ ' [ ' [ ', 1:list} */ $bar = [[], []]; foo($bar);' ], 'allowResourceInList' => [ ' $_s */ function foo(array $_s) : void { }' ], 'possiblyUndefinedObjectProperty' => [ 'value ?? "");' ], 'throwSelf' => [ ' [ ' 1, "b" => "two"]; }' ], 'falsableFunctionAllowedWhenBooleanExpected' => [ ' [ ' [ ' "literal"];', [ '$arr' => 'array{\'foo\\\\bar\nbaz\': string}' ] ], 'doubleSpaceBeforeAt' => [ ' [ ' [ ' [ 'format("Y");' ], 'intMaskWithClassConstants' => [ ' $flags */ function takesFlags(int $flags) : void { echo $flags; } takesFlags(FileFlag::MODIFIED | FileFlag::NEW);' ], 'intMaskOfWithClassWildcard' => [ ' $flags */ function takesFlags(int $flags) : void { echo $flags; } takesFlags(FileFlag::MODIFIED | FileFlag::NEW);' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { return [ 'invalidClassMethodReturn' => [ ' 'MissingDocblockType', ], 'invalidClassMethodReturnBrackets' => [ ' 'InvalidDocblock', ], 'invalidInterfaceMethodReturn' => [ ' 'MissingDocblockType', ], 'invalidInterfaceMethodReturnBrackets' => [ ' 'InvalidDocblock', ], 'invalidPropertyBrackets' => [ ' 'InvalidDocblock', ], 'invalidReturnClassWithComma' => [ ' 'InvalidDocblock', ], 'returnClassWithComma' => [ ' 'InvalidDocblock', ], 'missingParamType' => [ ' 'TooManyArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:21 - Too many arguments for fooBar ' . '- expecting 0 but saw 1', ], 'missingParamVar' => [ ' 'InvalidDocblock - src' . DIRECTORY_SEPARATOR . 'somefile.php:5:21 - Badly-formatted @param', ], 'invalidSlashWithString' => [ ' 'InvalidDocblock', ], 'missingReturnTypeWithBadDocblock' => [ ' 'MissingReturnType', [ 'InvalidDocblock' => \Psalm\Config::REPORT_INFO, ], ], 'invalidDocblockReturn' => [ ' 'MismatchingDocblockReturnType', ], 'intParamTypeDefinedInParent' => [ ' 'MissingParamType', 'error_levels' => ['MethodSignatureMismatch'], ], 'psalmInvalidVar' => [ ' */ public $foo = []; public function updateFoo(): void { $this->foo["boof"] = "hello"; } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'incorrectDocblockOrder' => [ ' 'MissingDocblockType', ], 'badlyFormattedVar' => [ ' 'InvalidDocblock', ], 'badlyWrittenVar' => [ 'conn()->method(); $myVar->otherMethod(); }', 'error_message' => 'MissingDocblockType', ], 'dontOverrideSameType' => [ ' 'InvalidReturnType', ], 'alwaysCheckReturnType' => [ ' 'UndefinedClass', ], 'preventBadBoolean' => [ ' 'UndefinedClass', ], 'undefinedDocblockClassCall' => [ 'foo()->bar(); } } ', 'error_message' => 'UndefinedDocblockClass', ], 'preventBadTKeyedArrayFormat' => [ ' 'InvalidDocblock', ], 'noPhpStormAnnotationsThankYou' => [ ' 'MismatchingDocblockParamType', ], 'noPhpStormAnnotationsPossiblyInvalid' => [ 'offsetGet("a"); }', 'error_message' => 'PossiblyInvalidMethodCall', ], 'doubleBar' => [ ' 'InvalidDocblock', ], 'badStringVar' => [ ' 'InvalidDocblock', ], 'badCallableVar' => [ ' 'InvalidDocblock', ], 'hyphenInType' => [ ' 'InvalidDocblock', ], 'badAmpersand' => [ ' 'InvalidDocblock', ], 'invalidTypeAlias' => [ ' */ class A {}', 'error_message' => 'InvalidDocblock', ], 'typeAliasInTKeyedArray' => [ ' 'InvalidReturnStatement', ], 'noCrashOnHalfDoneArrayPropertyType' => [ ' 'InvalidDocblock', ], 'noCrashOnHalfDoneTKeyedArrayPropertyType' => [ ' 'InvalidDocblock', ], 'noCrashOnInvalidClassTemplateAsType' => [ ' 'InvalidDocblock', ], 'noCrashOnInvalidFunctionTemplateAsType' => [ ' 'InvalidDocblock', ], 'returnTypeNewLineIsIgnored' => [ ' 'MissingReturnType', ], 'objectWithPropertiesAnnotationNoMatchingProperty' => [ 'foo; } class A {} foo(new A);', 'error_message' => 'InvalidArgument', ], 'badVar' => [ ' 'UndefinedDocblockClass', ], 'badPsalmType' => [ ' 'InvalidDocblock', ], 'mismatchingDocblockParamName' => [ ' 'InvalidDocblockParamName - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:41', ], 'nonEmptyArrayCalledWithEmpty' => [ ' $arr */ function foo(array $arr) : void { foreach ($arr as $a) {} echo $a; } foo([]);', 'error_message' => 'InvalidArgument', ], 'nonEmptyArrayCalledWithEmptyInNamespace' => [ ' $arr */ function foo(array $arr) : void { foreach ($arr as $a) {} echo $a; } foo([]);', 'error_message' => 'InvalidArgument', ], 'nonEmptyArrayCalledWithArray' => [ ' $arr */ function foo(array $arr) : void { foreach ($arr as $a) {} echo $a; } /** @param array $arr */ function bar(array $arr) { foo($arr); }', 'error_message' => 'ArgumentTypeCoercion', ], 'spreadOperatorArrayAnnotationBadArg' => [ ' 'InvalidScalarArgument', ], 'spreadOperatorArrayAnnotationBadSpreadArg' => [ ' 'InvalidScalarArgument', ], 'spreadOperatorByRefAnnotationBadCall1' => [ ' 'InvalidScalarArgument', ], 'spreadOperatorByRefAnnotationBadCall2' => [ ' 'InvalidScalarArgument', ], 'spreadOperatorByRefAnnotationBadCall3' => [ ' 'InvalidScalarArgument', ], 'identifyReturnType' => [ ' 'InvalidReturnType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:33', ], 'invalidParamDocblockAsterisk' => [ ' 'MissingDocblockType', ], 'canNeverReturnDeclaredType' => [ ' 'InvalidReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:32', ], 'falsableWithExpectedTypeTrue' => [ ' 'FalsableReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:32', ], 'DuplicatedParam' => [ ' 'InvalidDocblock - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:21 - Found duplicated @param or prefixed @param tag in docblock for bar', ], 'DuplicatedReturn' => [ ' 'InvalidDocblock - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:21 - Found duplicated @return or prefixed @return tag in docblock for bar', ], 'missingClassForTKeyedArray' => [ ' 'ImplementedReturnTypeMismatch' ], ]; } }