1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00
psalm/tests/MethodCallTest.php

679 lines
23 KiB
PHP
Raw Normal View History

2016-12-12 05:41:11 +01:00
<?php
namespace Psalm\Tests;
use function class_exists;
use const DIRECTORY_SEPARATOR;
class MethodCallTest extends TestCase
2016-12-12 05:41:11 +01:00
{
2018-11-06 03:57:36 +01:00
use Traits\InvalidCodeAnalysisTestTrait;
use Traits\ValidCodeAnalysisTestTrait;
/**
* @return void
*/
public function testExtendDocblockParamType()
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
return;
}
$this->addFile(
'somefile.php',
'<?php
new SoapFault("1", "faultstring", "faultactor");'
);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
2016-12-17 04:16:29 +01:00
/**
2019-03-01 21:55:20 +01:00
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
2016-12-17 04:16:29 +01:00
*/
2018-11-06 03:57:36 +01:00
public function providerValidCodeParse()
2016-12-17 04:16:29 +01:00
{
return [
'notInCallMapTest' => [
'<?php
Refactor scanning and analysis, introducing multithreading (#191) * Add failing test * Add visitor to soup up classlike references * Move a whole bunch of code into the visitor * Move some methods back, move onto analysis stage * Use the getAliases method everywhere * Fix refs * Fix more refs * Fix some tests * Fix more tests * Fix include tests * Shift config class finding to project checker and fix bugs * Fix a few more tests * transition test to new syntax * Remove var_dump * Delete a bunch of code and fix mutation test * Remove unnecessary visitation * Transition to better mocked out file provider, breaking some cached statement loading * Use different scheme for naming anonymous classes * Fix anonymous class issues * Refactor file/statement loading * Add specific property types * Fix mapped property assignment * Improve how we deal with traits * Fix trait checking * Pass Psalm checks * Add multi-process support * Delay console output until the end * Remove PHP 7 syntax * Update file storage with classes * Fix scanning individual files and add reflection return types * Always turn XDebug off * Add quicker method of getting method mutations * Queue return types for crawling * Interpret all strings as possible classes once we see a `get_class` call * Check invalid return types again * Fix template namespacing issues * Default to class-insensitive file names for includes * Don’t overwrite existing issues data * Add var docblocks for scanning * Add null check * Fix loading of external classes in templates * Only try to populate class when we haven’t yet seen it’s not a class * Fix trait property accessibility * Only ever improve docblock param type * Make param replacement more robust * Fix static const missing inferred type * Fix a few more tests * Register constant definitions * Fix trait aliasing * Skip constant type tests for now * Fix linting issues * Make sure caching is off for tests * Remove unnecessary return * Use emulative parser if on PHP 5.6 * Cache parser for faster first-time parse * Fix constant resolution when scanning classes * Remove test that’s beyond a practical scope * Add back --diff support * Add --help for --threads * Remove unused vars
2017-07-25 22:11:02 +02:00
new DOMImplementation();',
],
'parentStaticCall' => [
'<?php
class A {
/** @return void */
public static function foo(){}
}
class B extends A {
/** @return void */
public static function bar(){
parent::foo();
}
2017-05-27 02:05:57 +02:00
}',
],
'nonStaticInvocation' => [
'<?php
class Foo {
2018-01-11 21:50:45 +01:00
public static function barBar(): void {}
}
2017-05-27 02:05:57 +02:00
(new Foo())->barBar();',
],
'staticInvocation' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function fooFoo(): void {}
}
class B extends A {
}
2017-05-27 02:05:57 +02:00
B::fooFoo();',
],
'staticCallOnVar' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(): int {
return 5;
}
}
$foo = new A;
$b = $foo::bar();',
],
'uppercasedSelf' => [
'<?php
class X33{
2018-01-11 21:50:45 +01:00
public static function main(): void {
echo SELF::class . "\n"; // Class or interface SELF does not exist
}
}
X33::main();',
],
'dateTimeImmutableStatic' => [
'<?php
final class MyDate extends DateTimeImmutable {}
$today = new MyDate();
2018-01-05 17:50:27 +01:00
$yesterday = $today->sub(new DateInterval("P1D"));
$b = (new DateTimeImmutable())->modify("+3 hours");',
'assertions' => [
'$yesterday' => 'false|MyDate',
2018-01-05 17:50:27 +01:00
'$b' => 'DateTimeImmutable',
],
],
2018-01-22 06:17:16 +01:00
'magicCall' => [
'<?php
class A {
public function __call(string $method_name, array $args) {}
2018-01-22 06:17:16 +01:00
}
$a = new A;
$a->bar();',
],
'canBeCalledOnMagic' => [
'<?php
class A {
public function __call(string $method, array $args) {}
}
class B {}
$a = rand(0, 1) ? new A : new B;
$a->maybeUndefinedMethod();',
'assertions' => [],
'error_levels' => ['PossiblyUndefinedMethod'],
],
'canBeCalledOnMagicWithMethod' => [
'<?php
class A {
public function __call(string $method, array $args) {}
}
class B {
public function bar() : void {}
}
$a = rand(0, 1) ? new A : new B;
$a->bar();',
'assertions' => [],
],
2018-04-01 00:57:13 +02:00
'invokeCorrectType' => [
'<?php
class A {
public function __invoke(string $p): void {}
}
$q = new A;
$q("asda");',
],
'domDocumentAppendChild' => [
'<?php
$doc = new DOMDocument("1.0");
$node = $doc->createElement("foo");
if ($node instanceof DOMElement) {
$newnode = $doc->appendChild($node);
$newnode->setAttribute("bar", "baz");
}',
],
'nonStaticSelfCall' => [
'<?php
class A11 {
public function call() : self {
$result = self::method();
return $result;
}
public function method() : self {
return $this;
}
}
$x = new A11();
var_export($x->call());',
],
2018-06-06 05:42:02 +02:00
'simpleXml' => [
'<?php
$xml = new SimpleXMLElement("<a><b></b></a>");
$a = $xml->asXML();
$b = $xml->asXML("foo.xml");',
'assertions' => [
'$a' => 'string|false',
'$b' => 'bool',
2018-06-06 05:42:02 +02:00
],
],
'datetimeformatNotFalse' => [
'<?php
$format = random_bytes(10);
$dt = new DateTime;
$formatted = $dt->format($format);
if (false !== $formatted) {}
function takesString(string $s) : void {}
2019-03-23 19:27:54 +01:00
takesString($formatted);',
],
2018-07-06 05:02:09 +02:00
'domElement' => [
'<?php
function foo(DOMElement $e) : ?string {
$a = $e->getElementsByTagName("bar");
$b = $a->item(0);
if (!$b) {
return null;
}
return $b->getAttribute("bat");
}',
],
'domElementIteratorOrEmptyArray' => [
'<?php
function foo(string $XML) : void {
$dom = new DOMDocument();
$dom->loadXML($XML);
$elements = rand(0, 1) ? $dom->getElementsByTagName("bar") : [];
foreach ($elements as $element) {
$element->getElementsByTagName("bat");
}
2019-03-23 19:27:54 +01:00
}',
],
'reflectionParameter' => [
'<?php
function getTypeName(ReflectionParameter $parameter): string {
$type = $parameter->getType();
if ($type === null) {
return "mixed";
}
return $type->getName();
2019-03-23 19:27:54 +01:00
}',
],
'PDOMethod' => [
'<?php
function md5_and_reverse(string $string) : string {
return strrev(md5($string));
}
$db = new PDO("sqlite:sqlitedb");
$db->sqliteCreateFunction("md5rev", "md5_and_reverse", 1);',
],
2018-12-11 00:33:26 +01:00
'dontConvertedMaybeMixedAfterCall' => [
'<?php
class B {
public function foo() : void {}
}
/**
* @param array<B> $b
*/
function foo(array $a, array $b) : void {
$c = array_merge($b, $a);
foreach ($c as $d) {
$d->foo();
if ($d instanceof B) {}
}
}',
[],
'error_levels' => ['MixedAssignment', 'MixedMethodCall'],
],
'methodResolution' => [
'<?php
interface ReturnsString {
public function getId(): string;
}
/**
* @param mixed $a
*/
function foo(ReturnsString $user, $a): string {
strlen($user->getId());
(is_object($a) && method_exists($a, "getS")) ? (string)$a->getS() : "";
return $user->getId();
2019-03-23 19:27:54 +01:00
}',
],
'defineVariableCreatedInArgToMixed' => [
'<?php
function bar($a) : void {
if ($a->foo($b = (int) 5)) {
echo $b;
}
}',
[],
'error_levels' => ['MixedMethodCall', 'MissingParamType'],
],
'staticCallAfterMethodExists' => [
'<?php
class A
{
protected static function existing() : string
{
return "hello";
}
protected static function foo() : string
{
if (!method_exists(static::class, "maybeExists")) {
return "hello";
}
self::maybeExists();
return static::existing();
}
2019-03-23 19:27:54 +01:00
}',
],
'varSelfCall' => [
'<?php
class Foo {
/** @var self */
public static $current;
public function bar() : void {}
}
Foo::$current->bar();',
],
'pdoStatementSetFetchMode' => [
'<?php
class A {
/** @var ?string */
public $a;
}
$db = new PDO("sqlite::memory:");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare("select \"a\" as a");
$stmt->setFetchMode(PDO::FETCH_CLASS, A::class);
$stmt->execute();
/** @psalm-suppress MixedAssignment */
2019-07-05 22:24:00 +02:00
$a = $stmt->fetch();',
],
'datePeriodConstructor' => [
'<?php
function foo(DateTime $d1, DateTime $d2) : void {
new DatePeriod(
$d1,
DateInterval::createFromDateString("1 month"),
$d2
);
2019-07-05 22:24:00 +02:00
}',
],
'callMethodAfterCheckingExistence' => [
'<?php
class A {}
function foo(A $a) : void {
if (method_exists($a, "bar")) {
/** @psalm-suppress MixedArgument */
echo $a->bar();
}
}'
],
'callMethodAfterCheckingExistenceInClosure' => [
'<?php
class A {}
function foo(A $a) : void {
if (method_exists($a, "bar")) {
(function() use ($a) : void {
/** @psalm-suppress MixedArgument */
echo $a->bar();
})();
}
}'
],
'callManyMethodsAfterCheckingExistence' => [
'<?php
function foo(object $object) : void {
if (!method_exists($object, "foo")) {
return;
}
if (!method_exists($object, "bar")) {
return;
}
$object->foo();
$object->bar();
}'
],
'callManyMethodsAfterCheckingExistenceChained' => [
'<?php
function foo(object $object) : void {
if (method_exists($object, "foo") && method_exists($object, "bar")) {
$object->foo();
$object->bar();
}
}'
],
];
2016-12-17 04:16:29 +01:00
}
/**
2019-03-01 21:55:20 +01:00
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
*/
2018-11-06 03:57:36 +01:00
public function providerInvalidCodeParse()
{
return [
'staticInvocation' => [
'<?php
class Foo {
2018-01-11 21:50:45 +01:00
public function barBar(): void {}
}
Foo::barBar();',
2017-05-27 02:05:57 +02:00
'error_message' => 'InvalidStaticInvocation',
],
'parentStaticCall' => [
'<?php
class A {
/** @return void */
public function foo(){}
}
class B extends A {
/** @return void */
public static function bar(){
parent::foo();
}
}',
2017-05-27 02:05:57 +02:00
'error_message' => 'InvalidStaticInvocation',
],
'mixedMethodCall' => [
'<?php
class Foo {
2018-01-11 21:50:45 +01:00
public static function barBar(): void {}
}
/** @var mixed */
$a = (new Foo());
$a->barBar();',
'error_message' => 'MixedMethodCall',
'error_levels' => [
'MissingPropertyType',
2017-05-27 02:05:57 +02:00
'MixedAssignment',
],
],
'invalidMethodCall' => [
'<?php
("hello")->someMethod();',
'error_message' => 'InvalidMethodCall',
],
'possiblyInvalidMethodCall' => [
'<?php
class A1 {
public function methodOfA(): void {
}
}
/** @param A1|string $x */
function example($x, bool $isObject) : void {
if ($isObject) {
$x->methodOfA();
}
}',
'error_message' => 'PossiblyInvalidMethodCall',
],
'selfNonStaticInvocation' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public function fooFoo(): void {}
public static function barBar(): void {
self::fooFoo();
}
}',
2017-05-27 02:05:57 +02:00
'error_message' => 'NonStaticSelfCall',
],
'noParent' => [
'<?php
class Foo {
2018-01-11 21:50:45 +01:00
public function barBar(): void {
parent::barBar();
}
}',
2017-05-27 02:05:57 +02:00
'error_message' => 'ParentNotFound',
],
'coercedClass' => [
'<?php
class NullableClass {
}
class NullableBug {
/**
* @param class-string|null $className
* @return object|null
*/
public static function mock($className) {
if (!$className) { return null; }
return new $className();
}
/**
* @return ?NullableClass
*/
public function returns_nullable_class() {
return self::mock("NullableClass");
}
}',
'error_message' => 'LessSpecificReturnStatement',
'error_levels' => ['MixedInferredReturnType', 'MixedReturnStatement', 'TypeCoercion', 'MixedMethodCall'],
2017-05-27 02:05:57 +02:00
],
'undefinedVariableStaticCall' => [
'<?php
$foo::bar();',
'error_message' => 'UndefinedGlobalVariable',
],
'staticCallOnString' => [
'<?php
class A {
2018-01-11 21:50:45 +01:00
public static function bar(): int {
return 5;
}
}
$foo = "A";
/** @psalm-suppress InvalidStringClass */
$b = $foo::bar();',
'error_message' => 'MixedAssignment',
],
2018-01-22 06:17:16 +01:00
'possiblyNullFunctionCall' => [
'<?php
$this->foo();',
'error_message' => 'InvalidScope',
],
'possiblyFalseReference' => [
'<?php
class A {
public function bar(): void {}
}
$a = rand(0, 1) ? new A : false;
$a->bar();',
'error_message' => 'PossiblyFalseReference',
],
'undefinedParentClass' => [
'<?php
/**
* @psalm-suppress UndefinedClass
*/
class B extends A {}
$b = new B();',
'error_message' => 'MissingDependency - src' . DIRECTORY_SEPARATOR . 'somefile.php:7',
],
'variableMethodCallOnArray' => [
'<?php
$arr = [];
$b = "foo";
$arr->$b();',
'error_message' => 'InvalidMethodCall',
],
'intVarStaticCall' => [
'<?php
$a = 5;
$a::bar();',
'error_message' => 'UndefinedClass',
],
'intVarNewCall' => [
'<?php
$a = 5;
new $a();',
'error_message' => 'UndefinedClass',
],
2018-04-01 00:57:13 +02:00
'invokeTypeMismatch' => [
'<?php
class A {
public function __invoke(string $p): void {}
}
$q = new A;
$q(1);',
'error_message' => 'InvalidScalarArgument',
],
'explicitInvokeTypeMismatch' => [
'<?php
class A {
public function __invoke(string $p): void {}
}
(new A)->__invoke(1);',
'error_message' => 'InvalidScalarArgument',
],
'undefinedMethodPassedAsArg' => [
'<?php
class A {
public function __call(string $method, array $args) {}
}
$q = new A;
$q->foo(bar());',
2019-03-23 19:27:54 +01:00
'error_message' => 'UndefinedFunction',
],
'noIntersectionMethod' => [
'<?php
interface A {}
interface B {}
/** @param B&A $p */
function f($p): void {
$p->zugzug();
}',
2019-03-23 19:27:54 +01:00
'error_message' => 'UndefinedInterfaceMethod - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:29 - Method (B&A)::zugzug does not exist',
],
'noInstanceCallAsStatic' => [
'<?php
class C {
public function foo() : void {}
}
(new C)::foo();',
'error_message' => 'InvalidStaticInvocation',
],
'noExceptionOnMissingClass' => [
'<?php
/** @psalm-suppress UndefinedClass */
class A
{
/** @var class-string<Foo> */
protected $bar;
public function foo(string $s): void
{
$bar = $this->bar;
$bar::baz();
}
}',
2019-03-23 19:27:54 +01:00
'error_message' => 'UndefinedClass',
],
'checkMixedMethodCallStaticMethodCallArg' => [
'<?php
class B {}
/** @param mixed $a */
function foo($a) : void {
/** @psalm-suppress MixedMethodCall */
$a->bar(B::bat());
}',
'error_message' => 'UndefinedMethod',
],
'complainAboutUndefinedPropertyOnMixedCall' => [
'<?php
class C {
/** @param mixed $a */
public function foo($a) : void {
/** @psalm-suppress MixedMethodCall */
$a->bar($this->d);
}
}',
'error_message' => 'UndefinedThisPropertyFetch',
],
2019-07-23 00:04:52 +02:00
'complainAboutUndefinedPropertyOnMixedCallConcatOp' => [
'<?php
class A {
/**
* @psalm-suppress MixedMethodCall
*/
public function foo(object $a) : void {
$a->bar("bat" . $this->baz);
}
}',
'error_message' => 'UndefinedThisPropertyFetch',
],
'alreadyHasmethod' => [
'<?php
class A {
public function foo() : void {}
}
function foo(A $a) : void {
if (method_exists($a, "foo")) {
$object->foo();
}
}',
'error_message' => 'RedundantCondition',
],
];
}
2016-12-12 05:41:11 +01:00
}