2016-06-17 22:05:28 +02:00
|
|
|
<?php
|
2016-07-26 00:37:44 +02:00
|
|
|
namespace Psalm\Tests;
|
2016-06-17 22:05:28 +02:00
|
|
|
|
2016-10-11 04:49:43 +02:00
|
|
|
use PhpParser\ParserFactory;
|
2016-06-17 22:05:28 +02:00
|
|
|
use PHPUnit_Framework_TestCase;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Checker\FileChecker;
|
2016-10-11 04:49:43 +02:00
|
|
|
use Psalm\Checker\TypeChecker;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Config;
|
|
|
|
use Psalm\Context;
|
2016-10-11 04:49:43 +02:00
|
|
|
use Psalm\Type;
|
2016-06-17 22:05:28 +02:00
|
|
|
|
|
|
|
class TypeReconciliationTest extends PHPUnit_Framework_TestCase
|
|
|
|
{
|
2016-12-14 18:55:23 +01:00
|
|
|
/** @var \PhpParser\Parser */
|
2016-11-02 07:29:00 +01:00
|
|
|
protected static $parser;
|
2016-10-11 04:49:43 +02:00
|
|
|
|
|
|
|
public static function setUpBeforeClass()
|
|
|
|
{
|
2016-11-02 07:29:00 +01:00
|
|
|
self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
2016-10-11 04:49:43 +02:00
|
|
|
|
2016-12-14 18:28:38 +01:00
|
|
|
$config = new TestConfig();
|
2016-10-11 04:49:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function setUp()
|
|
|
|
{
|
2016-11-02 07:29:00 +01:00
|
|
|
FileChecker::clearCache();
|
2016-10-11 04:49:43 +02:00
|
|
|
}
|
|
|
|
|
2016-06-17 22:05:28 +02:00
|
|
|
public function testNotNull()
|
|
|
|
{
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject',
|
|
|
|
(string) TypeChecker::reconcileTypes('!null', Type::parseString('MyObject'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject',
|
|
|
|
(string) TypeChecker::reconcileTypes('!null', Type::parseString('MyObject|null'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject|false',
|
|
|
|
(string) TypeChecker::reconcileTypes('!null', Type::parseString('MyObject|false'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'mixed',
|
|
|
|
(string) TypeChecker::reconcileTypes('!null', Type::parseString('mixed'))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testNotEmpty()
|
|
|
|
{
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject',
|
|
|
|
(string) TypeChecker::reconcileTypes('!empty', Type::parseString('MyObject'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject',
|
|
|
|
(string) TypeChecker::reconcileTypes('!empty', Type::parseString('MyObject|null'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject',
|
|
|
|
(string) TypeChecker::reconcileTypes('!empty', Type::parseString('MyObject|false'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'mixed',
|
|
|
|
(string) TypeChecker::reconcileTypes('!empty', Type::parseString('mixed'))
|
|
|
|
);
|
|
|
|
|
2016-07-26 00:31:03 +02:00
|
|
|
// @todo in the future this should also work
|
|
|
|
/*
|
2016-06-17 22:05:28 +02:00
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject|true',
|
|
|
|
(string) TypeChecker::reconcileTypes('!empty', Type::parseString('MyObject|bool'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
2016-07-26 00:31:03 +02:00
|
|
|
*/
|
2016-06-17 22:05:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testNull()
|
|
|
|
{
|
|
|
|
$this->assertEquals(
|
|
|
|
'null',
|
2016-07-12 06:53:36 +02:00
|
|
|
(string) TypeChecker::reconcileTypes('null', Type::parseString('MyObject|null'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'null',
|
|
|
|
(string) TypeChecker::reconcileTypes('null', Type::parseString('mixed'))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testEmpty()
|
|
|
|
{
|
|
|
|
$this->assertEquals(
|
|
|
|
'null',
|
2016-07-12 06:53:36 +02:00
|
|
|
(string) TypeChecker::reconcileTypes('empty', Type::parseString('MyObject'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
$this->assertEquals(
|
|
|
|
'false',
|
2016-07-12 06:53:36 +02:00
|
|
|
(string) TypeChecker::reconcileTypes('empty', Type::parseString('MyObject|false'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'false',
|
2016-07-12 06:53:36 +02:00
|
|
|
(string) TypeChecker::reconcileTypes('empty', Type::parseString('MyObject|bool'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'mixed',
|
|
|
|
(string) TypeChecker::reconcileTypes('empty', Type::parseString('mixed'))
|
|
|
|
);
|
|
|
|
|
2016-12-14 18:55:23 +01:00
|
|
|
/** @var Type\Union */
|
2016-06-17 22:05:28 +02:00
|
|
|
$reconciled = TypeChecker::reconcileTypes('empty', Type::parseString('bool'));
|
|
|
|
$this->assertEquals('false', (string) $reconciled);
|
2016-07-26 00:37:44 +02:00
|
|
|
$this->assertInstanceOf('Psalm\Type\Atomic', $reconciled->types['false']);
|
2016-06-17 22:05:28 +02:00
|
|
|
}
|
|
|
|
|
2016-07-12 06:53:36 +02:00
|
|
|
public function testNotMyObject()
|
2016-06-17 22:05:28 +02:00
|
|
|
{
|
|
|
|
$this->assertEquals(
|
|
|
|
'bool',
|
2016-07-12 06:53:36 +02:00
|
|
|
(string) TypeChecker::reconcileTypes('!MyObject', Type::parseString('MyObject|bool'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'null',
|
2016-07-12 06:53:36 +02:00
|
|
|
(string) TypeChecker::reconcileTypes('!MyObject', Type::parseString('MyObject|null'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObjectB',
|
|
|
|
(string) TypeChecker::reconcileTypes('!MyObjectA', Type::parseString('MyObjectA|MyObjectB'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-07-12 06:53:36 +02:00
|
|
|
public function testMyObject()
|
2016-06-27 04:40:57 +02:00
|
|
|
{
|
2016-06-17 22:05:28 +02:00
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObject',
|
|
|
|
(string) TypeChecker::reconcileTypes('MyObject', Type::parseString('MyObject|bool'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
2016-07-12 06:53:36 +02:00
|
|
|
'MyObjectA',
|
|
|
|
(string) TypeChecker::reconcileTypes('MyObjectA', Type::parseString('MyObjectA|MyObjectB'))
|
2016-06-17 22:05:28 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-12-24 19:23:22 +01:00
|
|
|
public function testArrayContains()
|
|
|
|
{
|
|
|
|
$this->assertTrue(
|
|
|
|
TypeChecker::isContainedBy(
|
|
|
|
Type::parseString('array<string>'),
|
|
|
|
Type::parseString('array')
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertTrue(
|
|
|
|
TypeChecker::isContainedBy(
|
|
|
|
Type::parseString('array<Exception>'),
|
|
|
|
Type::parseString('array')
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertTrue(
|
|
|
|
TypeChecker::isContainedBy(
|
|
|
|
Type::parseString('array<UnexpectedValueException>'),
|
|
|
|
Type::parseString('array<Exception>')
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->assertFalse(
|
|
|
|
TypeChecker::isContainedBy(
|
|
|
|
Type::parseString('array<ValueException>'),
|
|
|
|
Type::parseString('array<UnexpectedException>')
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-12-24 00:52:34 +01:00
|
|
|
public function testNumeric()
|
|
|
|
{
|
|
|
|
$this->assertEquals(
|
|
|
|
'string',
|
|
|
|
(string) TypeChecker::reconcileTypes('numeric', Type::parseString('string'))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-12-11 19:48:11 +01:00
|
|
|
/**
|
|
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
|
|
* @expectedExceptionMessage TypeDoesNotContainType
|
|
|
|
*/
|
|
|
|
public function testMakeNonNullableNull()
|
2016-06-27 04:40:57 +02:00
|
|
|
{
|
2016-12-11 19:48:11 +01:00
|
|
|
$stmts = self::$parser->parse('<?php
|
|
|
|
class A { }
|
|
|
|
$a = new A();
|
|
|
|
if ($a === null) {
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
|
|
|
$context = new Context('somefile.php');
|
|
|
|
$file_checker->check(true, true, $context);
|
2016-06-17 22:05:28 +02:00
|
|
|
}
|
2016-10-11 04:49:43 +02:00
|
|
|
|
2016-12-12 05:41:11 +01:00
|
|
|
/**
|
|
|
|
* @expectedException \Psalm\Exception\CodeException
|
2016-12-12 19:50:46 +01:00
|
|
|
* @expectedExceptionMessage TypeDoesNotContainType
|
|
|
|
*/
|
|
|
|
public function testMakeInstanceOfThingInElseif()
|
|
|
|
{
|
|
|
|
$stmts = self::$parser->parse('<?php
|
|
|
|
class A { }
|
|
|
|
class B { }
|
|
|
|
class C { }
|
|
|
|
$a = rand(0, 10) > 5 ? new A() : new B();
|
|
|
|
if ($a instanceof A) {
|
|
|
|
} elseif ($a instanceof C) {
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
|
|
|
$context = new Context('somefile.php');
|
|
|
|
$file_checker->check(true, true, $context);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @expectedException \Psalm\Exception\CodeException
|
2016-12-12 05:41:11 +01:00
|
|
|
* @expectedExceptionMessage FailedTypeResolution
|
|
|
|
*/
|
|
|
|
public function testFailedTypeResolution()
|
|
|
|
{
|
|
|
|
$stmts = self::$parser->parse('<?php
|
|
|
|
class A { }
|
|
|
|
$a = new A();
|
|
|
|
if ($a instanceof A) {
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
|
|
|
$context = new Context('somefile.php');
|
|
|
|
$file_checker->check(true, true, $context);
|
|
|
|
}
|
|
|
|
|
2016-10-11 04:49:43 +02:00
|
|
|
public function testNotInstanceOf()
|
|
|
|
{
|
2016-11-02 07:29:00 +01:00
|
|
|
$stmts = self::$parser->parse('<?php
|
2016-10-11 04:49:43 +02:00
|
|
|
class A { }
|
|
|
|
|
|
|
|
class B extends A { }
|
|
|
|
|
|
|
|
$out = null;
|
|
|
|
|
|
|
|
if ($a instanceof B) {
|
|
|
|
// do something
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$out = $a;
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
2016-10-11 04:49:43 +02:00
|
|
|
$context = new Context('somefile.php');
|
2016-10-15 19:10:05 +02:00
|
|
|
$context->vars_in_scope['$a'] = Type::parseString('A');
|
2016-10-11 04:49:43 +02:00
|
|
|
$file_checker->check(true, true, $context);
|
2016-10-15 19:10:05 +02:00
|
|
|
$this->assertEquals('null|A', (string) $context->vars_in_scope['$out']);
|
2016-10-11 04:49:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testNotInstanceOfProperty()
|
|
|
|
{
|
2016-11-02 07:29:00 +01:00
|
|
|
$stmts = self::$parser->parse('<?php
|
2016-10-11 04:49:43 +02:00
|
|
|
class B { }
|
|
|
|
|
|
|
|
class C extends B { }
|
|
|
|
|
|
|
|
class A {
|
|
|
|
/** @var B */
|
|
|
|
public $foo;
|
|
|
|
}
|
|
|
|
|
|
|
|
$out = null;
|
|
|
|
|
|
|
|
if ($a->foo instanceof C) {
|
|
|
|
// do something
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$out = $a->foo;
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
2016-10-11 04:49:43 +02:00
|
|
|
$context = new Context('somefile.php');
|
2016-10-15 19:10:05 +02:00
|
|
|
$context->vars_in_scope['$a'] = Type::parseString('A');
|
2016-10-11 04:49:43 +02:00
|
|
|
$file_checker->check(true, true, $context);
|
2016-10-15 19:10:05 +02:00
|
|
|
$this->assertEquals('null|B', (string) $context->vars_in_scope['$out']);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testNotInstanceOfPropertyElseif()
|
|
|
|
{
|
2016-11-02 07:29:00 +01:00
|
|
|
$stmts = self::$parser->parse('<?php
|
2016-10-15 19:10:05 +02:00
|
|
|
class B { }
|
|
|
|
|
|
|
|
class C extends B { }
|
|
|
|
|
|
|
|
class A {
|
|
|
|
/** @var string|B */
|
|
|
|
public $foo;
|
|
|
|
}
|
|
|
|
|
|
|
|
$out = null;
|
|
|
|
|
|
|
|
if (is_string($a->foo)) {
|
|
|
|
|
|
|
|
}
|
|
|
|
elseif ($a->foo instanceof C) {
|
|
|
|
// do something
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$out = $a->foo;
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
2016-10-15 19:10:05 +02:00
|
|
|
$context = new Context('somefile.php');
|
|
|
|
$context->vars_in_scope['$a'] = Type::parseString('A');
|
|
|
|
$file_checker->check(true, true, $context);
|
|
|
|
$this->assertEquals('null|B', (string) $context->vars_in_scope['$out']);
|
2016-10-11 04:49:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testTypeArguments()
|
|
|
|
{
|
2016-11-02 07:29:00 +01:00
|
|
|
$stmts = self::$parser->parse('<?php
|
2016-10-11 04:49:43 +02:00
|
|
|
$a = min(0, 1);
|
|
|
|
$b = min([0, 1]);
|
|
|
|
$c = min("a", "b");
|
|
|
|
');
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
2016-10-11 04:49:43 +02:00
|
|
|
$context = new Context('somefile.php');
|
|
|
|
$file_checker->check(true, true, $context);
|
2016-12-17 00:56:23 +01:00
|
|
|
$this->assertEquals('int', (string) $context->vars_in_scope['$a']);
|
|
|
|
$this->assertEquals('int', (string) $context->vars_in_scope['$b']);
|
|
|
|
$this->assertEquals('string', (string) $context->vars_in_scope['$c']);
|
2016-10-11 04:49:43 +02:00
|
|
|
}
|
2016-12-24 00:52:34 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @expectedException \Psalm\Exception\CodeException
|
|
|
|
* @expectedExceptionMessage TypeDoesNotContainType
|
|
|
|
*/
|
|
|
|
public function testTypeTransformation()
|
|
|
|
{
|
|
|
|
$stmts = self::$parser->parse('<?php
|
|
|
|
$a = "5";
|
|
|
|
|
|
|
|
if (is_numeric($a)) {
|
|
|
|
if (is_int($a)) {
|
|
|
|
echo $a;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
|
|
|
|
$file_checker = new FileChecker('somefile.php', $stmts);
|
|
|
|
$context = new Context('somefile.php');
|
|
|
|
$file_checker->check(true, true, $context);
|
|
|
|
}
|
2016-06-17 22:05:28 +02:00
|
|
|
}
|